From e0d83ca027197141abc2d0bfb2ecdefa3c75b92c Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Fri, 19 Jun 2026 12:33:20 +0200 Subject: [PATCH 1/2] fix: Respect exception autocapture default for contexts --- .changeset/calm-contexts-capture.md | 5 ++ posthog/__init__.py | 8 +-- posthog/client.py | 4 +- posthog/contexts.py | 33 +++++++++-- posthog/test/test_contexts.py | 89 +++++++++++++++++++++++++++-- references/public_api_snapshot.txt | 10 ++-- 6 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 .changeset/calm-contexts-capture.md diff --git a/.changeset/calm-contexts-capture.md b/.changeset/calm-contexts-capture.md new file mode 100644 index 00000000..c92828d5 --- /dev/null +++ b/.changeset/calm-contexts-capture.md @@ -0,0 +1,5 @@ +--- +'pypi/posthog': patch +--- + +Respect exception autocapture defaults for new contexts. diff --git a/posthog/__init__.py b/posthog/__init__.py index 9f0b4893..0f758baa 100644 --- a/posthog/__init__.py +++ b/posthog/__init__.py @@ -76,7 +76,7 @@ def new_context( fresh: bool = False, - capture_exceptions: bool = True, + capture_exceptions: Optional[bool] = None, client: Optional[Client] = None, ): """ @@ -84,7 +84,7 @@ def new_context( Args: fresh: Whether to start with a fresh context (default: False) - capture_exceptions: Whether to capture exceptions raised within the context (default: True) + capture_exceptions: Whether to capture exceptions raised within the context. If omitted, defaults to the relevant client's exception autocapture setting. client: Optional Posthog client instance to use for this context (default: None) Examples: @@ -103,13 +103,13 @@ def new_context( ) -def scoped(fresh=False, capture_exceptions=True): +def scoped(fresh=False, capture_exceptions: Optional[bool] = None): """ Decorator that creates a new context for the function. Args: fresh: Whether to start with a fresh context (default: False) - capture_exceptions: Whether to capture and track exceptions with posthog error tracking (default: True) + capture_exceptions: Whether to capture and track exceptions with posthog error tracking. If omitted, defaults to the global exception autocapture setting. Examples: ```python diff --git a/posthog/client.py b/posthog/client.py index 800cc577..95c8ebf4 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -456,13 +456,13 @@ def _set_before_send(self, before_send): else: self.before_send = None - def new_context(self, fresh=False, capture_exceptions=True): + def new_context(self, fresh=False, capture_exceptions: Optional[bool] = None): """ Create a new context for managing shared state. Learn more about [contexts](/docs/libraries/python#contexts). Args: fresh: Whether to create a fresh context that doesn't inherit from parent. - capture_exceptions: Whether to automatically capture exceptions in this context. + capture_exceptions: Whether to automatically capture exceptions in this context. If omitted, defaults to this client's exception autocapture setting. Examples: ```python diff --git a/posthog/contexts.py b/posthog/contexts.py index 56bed4a9..99b21562 100644 --- a/posthog/contexts.py +++ b/posthog/contexts.py @@ -112,10 +112,25 @@ def _get_current_context() -> Optional[ContextScope]: return _context_stack.get() +def _default_capture_exceptions(client: Optional["Client"] = None) -> bool: + if client is not None: + return client.enable_exception_autocapture + + import posthog + + default_client = getattr(posthog, "default_client", None) + if default_client is not None: + client_default = getattr(default_client, "enable_exception_autocapture", None) + if isinstance(client_default, bool): + return client_default + + return posthog.enable_exception_autocapture + + @contextmanager def new_context( fresh: bool = False, - capture_exceptions: bool = True, + capture_exceptions: Optional[bool] = None, client: Optional["Client"] = None, ): """ @@ -127,7 +142,8 @@ def new_context( fresh: Whether to start with a fresh context (default: False). If False, inherits tags, identity and session id's from parent context. If True, starts with no state - capture_exceptions: Whether to capture exceptions raised within the context (default: True). + capture_exceptions: Whether to capture exceptions raised within the context. + If omitted, defaults to the relevant client's exception autocapture setting. If True, captures exceptions and tags them with the context tags before propagating them. If False, exceptions will propagate without being tagged or captured. client: Optional client instance to use for capturing exceptions (default: None). @@ -162,7 +178,14 @@ def new_context( from posthog import capture_exception current_context = _get_current_context() - new_context = ContextScope(current_context, fresh, capture_exceptions, client) + resolved_capture_exceptions = ( + capture_exceptions + if capture_exceptions is not None + else _default_capture_exceptions(client) + ) + new_context = ContextScope( + current_context, fresh, resolved_capture_exceptions, client + ) _context_stack.set(new_context) try: @@ -370,14 +393,14 @@ def get_code_variables_ignore_patterns_context() -> Optional[list]: F = TypeVar("F", bound=Callable[..., Any]) -def scoped(fresh: bool = False, capture_exceptions: bool = True): +def scoped(fresh: bool = False, capture_exceptions: Optional[bool] = None): """ Decorator that creates a new context for the function. Simply wraps the function in a with posthog.new_context(): block. Args: fresh: Whether to start with a fresh context (default: False) - capture_exceptions: Whether to capture and track exceptions with posthog error tracking (default: True) + capture_exceptions: Whether to capture and track exceptions with posthog error tracking. If omitted, defaults to the global exception autocapture setting. Example: @posthog.scoped() diff --git a/posthog/test/test_contexts.py b/posthog/test/test_contexts.py index a2f81fba..4e61ffe0 100644 --- a/posthog/test/test_contexts.py +++ b/posthog/test/test_contexts.py @@ -1,7 +1,9 @@ import asyncio import unittest -from unittest.mock import patch +from unittest.mock import MagicMock, patch +import posthog +from posthog.client import Client from posthog.contexts import ( get_tags, new_context, @@ -101,7 +103,7 @@ def check_context_on_capture(exception, **kwargs): if is_async: - @scoped() + @scoped(capture_exceptions=True) async def failing_function(): tag("important_context", "value") raise test_exception @@ -111,7 +113,7 @@ def run(): else: - @scoped() + @scoped(capture_exceptions=True) def failing_function(): tag("important_context", "value") raise test_exception @@ -146,7 +148,7 @@ def check_context_on_capture(exception, **kwargs): tag("outer_context", "outer_value") try: - with new_context(): + with new_context(capture_exceptions=True): tag("inner_context", "inner_value") raise test_exception except RuntimeError: @@ -158,6 +160,85 @@ def check_context_on_capture(exception, **kwargs): # Verify capture_exception was called mock_capture.assert_called_once_with(test_exception) + @patch("posthog.capture_exception") + def test_new_context_defaults_to_global_exception_autocapture_disabled( + self, mock_capture + ): + original_default_client = posthog.default_client + original_enable_exception_autocapture = posthog.enable_exception_autocapture + posthog.default_client = None + posthog.enable_exception_autocapture = False + test_exception = RuntimeError("Context exception") + + try: + with self.assertRaises(RuntimeError): + with posthog.new_context(): + raise test_exception + finally: + posthog.default_client = original_default_client + posthog.enable_exception_autocapture = original_enable_exception_autocapture + + mock_capture.assert_not_called() + + def test_new_context_defaults_to_custom_client_exception_autocapture_disabled(self): + client = Client( + "phc_test", + sync_mode=True, + disabled=True, + enable_exception_autocapture=False, + ) + client.capture_exception = MagicMock() + test_exception = RuntimeError("Context exception") + + try: + with self.assertRaises(RuntimeError): + with client.new_context(): + raise test_exception + finally: + client.shutdown() + + client.capture_exception.assert_not_called() + + @patch("posthog.capture_exception") + def test_new_context_explicit_true_captures_when_global_autocapture_disabled( + self, mock_capture + ): + original_default_client = posthog.default_client + original_enable_exception_autocapture = posthog.enable_exception_autocapture + posthog.default_client = None + posthog.enable_exception_autocapture = False + test_exception = RuntimeError("Context exception") + + try: + with self.assertRaises(RuntimeError): + with posthog.new_context(capture_exceptions=True): + raise test_exception + finally: + posthog.default_client = original_default_client + posthog.enable_exception_autocapture = original_enable_exception_autocapture + + mock_capture.assert_called_once_with(test_exception) + + @patch("posthog.capture_exception") + def test_new_context_explicit_false_skips_capture_when_global_autocapture_enabled( + self, mock_capture + ): + original_default_client = posthog.default_client + original_enable_exception_autocapture = posthog.enable_exception_autocapture + posthog.default_client = None + posthog.enable_exception_autocapture = True + test_exception = RuntimeError("Context exception") + + try: + with self.assertRaises(RuntimeError): + with posthog.new_context(capture_exceptions=False): + raise test_exception + finally: + posthog.default_client = original_default_client + posthog.enable_exception_autocapture = original_enable_exception_autocapture + + mock_capture.assert_not_called() + def test_identify_context(self): with new_context(fresh=True): # Initially no distinct ID diff --git a/references/public_api_snapshot.txt b/references/public_api_snapshot.txt index 2ba8a253..42180d2b 100644 --- a/references/public_api_snapshot.txt +++ b/references/public_api_snapshot.txt @@ -870,8 +870,8 @@ function posthog.contexts.get_context_distinct_id() -> Optional[str] function posthog.contexts.get_context_session_id() -> Optional[str] function posthog.contexts.get_tags() -> Dict[str, Any] function posthog.contexts.identify_context(distinct_id: str) -> None -function posthog.contexts.new_context(fresh: bool = False, capture_exceptions: bool = True, client: Optional[Client] = None) -function posthog.contexts.scoped(fresh: bool = False, capture_exceptions: bool = True) +function posthog.contexts.new_context(fresh: bool = False, capture_exceptions: Optional[bool] = None, client: Optional[Client] = None) +function posthog.contexts.scoped(fresh: bool = False, capture_exceptions: Optional[bool] = None) function posthog.contexts.set_capture_exception_code_variables_context(enabled: bool) -> None function posthog.contexts.set_code_variables_ignore_patterns_context(ignore_patterns: list) -> None function posthog.contexts.set_code_variables_mask_patterns_context(mask_patterns: list) -> None @@ -939,7 +939,7 @@ function posthog.identify_context(distinct_id: str) function posthog.integrations.django.markcoroutinefunction(func) function posthog.join() -> None function posthog.load_feature_flags() -function posthog.new_context(fresh: bool = False, capture_exceptions: bool = True, client: Optional[Client] = None) +function posthog.new_context(fresh: bool = False, capture_exceptions: Optional[bool] = None, client: Optional[Client] = None) function posthog.request.batch_post(api_key: str, host: Optional[str] = None, gzip: bool = False, timeout: int = 15, path: str = EVENTS_ENDPOINT, **kwargs) -> requests.Response function posthog.request.determine_server_host(host: Optional[str]) -> str function posthog.request.disable_connection_reuse() -> None @@ -952,7 +952,7 @@ function posthog.request.post(api_key: str, host: Optional[str] = None, path: Op function posthog.request.remote_config(personal_api_key: str, project_api_key: str, host: Optional[str] = None, key: str = '', timeout: int = 15) -> Any function posthog.request.reset_sessions() -> None function posthog.request.set_socket_options(socket_options: Optional[SocketOptions]) -> None -function posthog.scoped(fresh=False, capture_exceptions=True) +function posthog.scoped(fresh=False, capture_exceptions: Optional[bool] = None) function posthog.set(**kwargs: Unpack[OptionalSetArgs]) -> Optional[str] function posthog.set_capture_exception_code_variables_context(enabled: bool) function posthog.set_code_variables_ignore_patterns_context(ignore_patterns: list) @@ -1062,7 +1062,7 @@ method posthog.client.Client.get_remote_config_payload(key: str) method posthog.client.Client.group_identify(group_type: str, group_key: str, properties: Optional[Dict[str, Any]] = None, timestamp: Optional[Union[datetime, str]] = None, uuid: Optional[str] = None, disable_geoip: Optional[bool] = None, distinct_id: Optional[ID_TYPES] = None) -> Optional[str] method posthog.client.Client.join() -> None method posthog.client.Client.load_feature_flags() -method posthog.client.Client.new_context(fresh=False, capture_exceptions=True) +method posthog.client.Client.new_context(fresh=False, capture_exceptions: Optional[bool] = None) method posthog.client.Client.set(**kwargs: Unpack[OptionalSetArgs]) -> Optional[str] method posthog.client.Client.set_once(**kwargs: Unpack[OptionalSetArgs]) -> Optional[str] method posthog.client.Client.shutdown() -> None From 2c20ebdd0e5a5a9c3a6973a6e384024aaeb5ef12 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Fri, 19 Jun 2026 14:48:37 +0200 Subject: [PATCH 2/2] address pr review feedback --- posthog/test/test_contexts.py | 94 +++++++++++++++-------------------- 1 file changed, 39 insertions(+), 55 deletions(-) diff --git a/posthog/test/test_contexts.py b/posthog/test/test_contexts.py index 4e61ffe0..4c7aa557 100644 --- a/posthog/test/test_contexts.py +++ b/posthog/test/test_contexts.py @@ -161,24 +161,48 @@ def check_context_on_capture(exception, **kwargs): mock_capture.assert_called_once_with(test_exception) @patch("posthog.capture_exception") - def test_new_context_defaults_to_global_exception_autocapture_disabled( + def test_new_context_respects_global_exception_autocapture_setting( self, mock_capture ): - original_default_client = posthog.default_client - original_enable_exception_autocapture = posthog.enable_exception_autocapture - posthog.default_client = None - posthog.enable_exception_autocapture = False - test_exception = RuntimeError("Context exception") - - try: - with self.assertRaises(RuntimeError): - with posthog.new_context(): - raise test_exception - finally: - posthog.default_client = original_default_client - posthog.enable_exception_autocapture = original_enable_exception_autocapture + cases = [ + (False, None, False), + (False, True, True), + (True, False, False), + ] - mock_capture.assert_not_called() + for global_setting, explicit_arg, expect_captured in cases: + with self.subTest( + global_setting=global_setting, + capture_exceptions=explicit_arg, + expect_captured=expect_captured, + ): + original_default_client = posthog.default_client + original_enable_exception_autocapture = ( + posthog.enable_exception_autocapture + ) + posthog.default_client = None + posthog.enable_exception_autocapture = global_setting + test_exception = RuntimeError("Context exception") + + try: + with self.assertRaises(RuntimeError): + kwargs = {} + if explicit_arg is not None: + kwargs["capture_exceptions"] = explicit_arg + + with posthog.new_context(**kwargs): + raise test_exception + finally: + posthog.default_client = original_default_client + posthog.enable_exception_autocapture = ( + original_enable_exception_autocapture + ) + + if expect_captured: + mock_capture.assert_called_once_with(test_exception) + else: + mock_capture.assert_not_called() + mock_capture.reset_mock() def test_new_context_defaults_to_custom_client_exception_autocapture_disabled(self): client = Client( @@ -199,46 +223,6 @@ def test_new_context_defaults_to_custom_client_exception_autocapture_disabled(se client.capture_exception.assert_not_called() - @patch("posthog.capture_exception") - def test_new_context_explicit_true_captures_when_global_autocapture_disabled( - self, mock_capture - ): - original_default_client = posthog.default_client - original_enable_exception_autocapture = posthog.enable_exception_autocapture - posthog.default_client = None - posthog.enable_exception_autocapture = False - test_exception = RuntimeError("Context exception") - - try: - with self.assertRaises(RuntimeError): - with posthog.new_context(capture_exceptions=True): - raise test_exception - finally: - posthog.default_client = original_default_client - posthog.enable_exception_autocapture = original_enable_exception_autocapture - - mock_capture.assert_called_once_with(test_exception) - - @patch("posthog.capture_exception") - def test_new_context_explicit_false_skips_capture_when_global_autocapture_enabled( - self, mock_capture - ): - original_default_client = posthog.default_client - original_enable_exception_autocapture = posthog.enable_exception_autocapture - posthog.default_client = None - posthog.enable_exception_autocapture = True - test_exception = RuntimeError("Context exception") - - try: - with self.assertRaises(RuntimeError): - with posthog.new_context(capture_exceptions=False): - raise test_exception - finally: - posthog.default_client = original_default_client - posthog.enable_exception_autocapture = original_enable_exception_autocapture - - mock_capture.assert_not_called() - def test_identify_context(self): with new_context(fresh=True): # Initially no distinct ID