Skip to content
Merged
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 .changeset/calm-contexts-capture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'pypi/posthog': patch
---

Respect exception autocapture defaults for new contexts.
8 changes: 4 additions & 4 deletions posthog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@

def new_context(
fresh: bool = False,
capture_exceptions: bool = True,
capture_exceptions: Optional[bool] = None,
client: Optional[Client] = None,
):
"""
Create a new context scope that will be active for the duration of the with block.

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:
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,13 +476,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
Expand Down
33 changes: 28 additions & 5 deletions posthog/contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
):
"""
Expand All @@ -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).
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down
73 changes: 69 additions & 4 deletions posthog/test/test_contexts.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -111,7 +113,7 @@ def run():

else:

@scoped()
@scoped(capture_exceptions=True)
def failing_function():
tag("important_context", "value")
raise test_exception
Expand Down Expand Up @@ -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:
Expand All @@ -158,6 +160,69 @@ 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_respects_global_exception_autocapture_setting(
self, mock_capture
):
cases = [
(False, None, False),
(False, True, True),
(True, False, False),
]

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(
"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()

def test_identify_context(self):
with new_context(fresh=True):
# Initially no distinct ID
Expand Down
10 changes: 5 additions & 5 deletions references/public_api_snapshot.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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[Union[str, UUID]] = 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
Expand Down
Loading