Skip to content

Commit dcceb79

Browse files
committed
auto-capture in with blocks
1 parent b5e71bb commit dcceb79

File tree

3 files changed

+50
-41
lines changed

3 files changed

+50
-41
lines changed

example.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -105,26 +105,21 @@
105105

106106

107107
# You can add tags to a context, and these are automatically added to exceptions captured within that context.
108-
# Recommended: you can use the `@posthog.scoped` decorator to enter a new context.
108+
# You can enter a new context using a with statement. Any exceptions thrown in the context will be captured,
109+
# and tagged with the context tags
110+
with posthog.new_context():
111+
posthog.tag("user_id", 123)
112+
posthog.tag("transaction_id", "abc123")
113+
posthog.tag("some_arbitrary_value", {"tags": "can be dicts"})
114+
# This exception will be captured with the tags set above
115+
raise Exception("Order processing failed")
116+
117+
118+
# You can also use the `@posthog.scoped` decorator to enter a new context.
109119
@posthog.scoped
110120
def process_order(order_id):
111121
posthog.tag("order_id", order_id)
112122
# Exception will be captured and tagged automatically
113123
raise Exception("Order processing failed")
114124

115-
116-
# You can also use the directly context manager, but you should prefer to use the decorator where possible,
117-
# because it's tricky to remember to always capture the exception within the context scope.
118-
with posthog.new_context():
119-
posthog.tag("user_id", "123")
120-
posthog.tag("transaction_id", "abc123")
121-
try:
122-
# Any exception within this block will include these tags
123-
raise Exception("Order processing failed")
124-
except Exception as e:
125-
# Note the exception must be captured within the scope for the tags
126-
# to be applied
127-
posthog.capture_exception(e)
128-
129-
130125
posthog.shutdown()

posthog/scopes.py

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import contextvars
22
from contextlib import contextmanager
33
from typing import Any, Callable, Dict, TypeVar, cast
4+
from posthog import capture_exception
45

56
_context_stack: contextvars.ContextVar[list] = contextvars.ContextVar("posthog_context_stack", default=[{}])
67

@@ -15,30 +16,24 @@ def new_context():
1516
# but right now it only applies to exceptions...
1617
"""
1718
Create a new context scope that will be active for the duration of the with block.
18-
Any tags set within this scope will be isolated to this context. Tags added to a
19-
context will be added to exceptions captured within that context.
20-
21-
NOTE: tags set within a context will only be added to exceptions captured within that
22-
context - ensure you call `posthog.capture_exception()` before the end of the with
23-
block, or the extra tags will be lost.
24-
25-
It's strongly recommended to use the `posthog.tracked` decorator to instrument functions, rather
26-
than directly using this context manager.
19+
Any tags set within this scope will be isolated to this context. Any exceptions raised
20+
within the context will be captured and tagged with the context tags.
2721
2822
Example:
2923
with posthog.new_context():
3024
posthog.tag("user_id", "123")
31-
try:
32-
# Do something that might raise an exception
33-
except Exception as e:
34-
posthog.capture_exception(e)
35-
raise e
25+
# The exception will be captured and tagged with the context tags
26+
raise ValueError("Something went wrong")
27+
3628
"""
3729
current_stack = _context_stack.get()
3830
new_stack = current_stack + [{}]
3931
token = _context_stack.set(new_stack)
4032
try:
4133
yield
34+
except Exception as e:
35+
capture_exception(e)
36+
raise
4237
finally:
4338
_context_stack.reset(token)
4439

@@ -80,8 +75,7 @@ def scoped(func: F) -> F:
8075
"""
8176
Decorator that creates a new context for the function, wraps the function in a
8277
try/except block, and if an exception occurs, captures it with the current context
83-
tags before re-raising it. This is the recommended way to wrap/track functions for
84-
posthog error tracking.
78+
tags before re-raising it.
8579
8680
Args:
8781
func: The function to wrap
@@ -96,17 +90,10 @@ def process_payment(payment_id):
9690
"""
9791
from functools import wraps
9892

99-
import posthog
100-
10193
@wraps(func)
10294
def wrapper(*args, **kwargs):
10395
with new_context():
104-
try:
105-
return func(*args, **kwargs)
106-
except Exception as e:
107-
# Capture the exception with current context tags
108-
# The capture_exception function will handle deduplication
109-
posthog.capture_exception(e, properties=get_tags())
110-
raise # Re-raise the exception after capturing it
96+
return func(*args, **kwargs)
97+
11198

11299
return cast(F, wrapper)

posthog/test/test_scopes.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,30 @@ def failing_function():
105105

106106
# Context should be cleared after function execution
107107
self.assertEqual(get_tags(), {})
108+
109+
@patch("posthog.capture_exception")
110+
def test_new_context_exception_handling(self, mock_capture):
111+
test_exception = RuntimeError("Context exception")
112+
113+
# Set up outer context
114+
tag("outer_context", "outer_value")
115+
116+
try:
117+
with new_context():
118+
tag("inner_context", "inner_value")
119+
raise test_exception
120+
except RuntimeError:
121+
pass # Expected exception
122+
123+
# Exception should be captured with inner context
124+
mock_capture.assert_called_once()
125+
args, kwargs = mock_capture.call_args
126+
127+
# Check that the exception was passed
128+
self.assertEqual(args[0], test_exception)
129+
130+
# Check that the inner context was included in properties
131+
self.assertEqual(kwargs.get("properties", {}).get("inner_context"), "inner_value")
132+
133+
# Outer context should still be intact
134+
self.assertEqual(get_tags()["outer_context"], "outer_value")

0 commit comments

Comments
 (0)