Skip to content

Commit 572f08a

Browse files
committed
docs and fixes
1 parent 1c1c528 commit 572f08a

9 files changed

Lines changed: 122 additions & 62 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ This release contains a number of major breaking changes:
66
- fix: remove `identify` (prefer `posthog.set()`), and `page` and `screen` (prefer `posthog.capture()`)
77
- fix: delete exception-capture specific integrations module. Prefer the general-purpose django middleware as a replacement for the django `Integration`.
88

9+
To migrate to this version, you'll mostly just need to switch to using named keyword arguments, rather than positional ones. For example:
10+
```python
11+
# Old calling convention
12+
posthog.capture("user123", "button_clicked", {"button_id": "123"})
13+
# New calling convention
14+
posthog.capture(distinct_id="user123", event="button_clicked", properties={"button_id": "123"})
15+
16+
# Better pattern
17+
with posthog.new_context():
18+
posthog.identify_context("user123")
19+
20+
# The event name is the first argument, and can be passed positionally, or as a keyword argument in a later position
21+
posthog.capture("button_pressed")
22+
```
23+
24+
Generally, arguments are now appropriately typed, and docstrings have been updated. If something is unclear, please open an issue, or submit a PR!
25+
926
# 5.4.0 - 2025-06-20
1027

1128
- feat: add support to session_id context on page method

posthog/__init__.py

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from posthog.args import OptionalCaptureArgs, OptionalSetArgs, ExceptionArg
66
from posthog.client import Client
7-
from posthog.scopes import (
7+
from posthog.contexts import (
88
new_context as inner_new_context,
99
scoped as inner_scoped,
1010
tag as inner_tag,
@@ -64,7 +64,7 @@ def tag(name: str, value: Any):
6464
default_client = None # type: Optional[Client]
6565

6666

67-
# NOTE - this and following functions take and unpack kwargs because we needed to make
67+
# NOTE - this and following functions take unpacked kwargs because we needed to make
6868
# it impossible to write `posthog.capture(distinct-id, event-name)` - basically, to enforce
6969
# the breaking change made between 5.3.0 and 6.0.0. This decision can be unrolled in later
7070
# versions, without a breaking change, to get back the type information in function signatures
@@ -76,17 +76,33 @@ def capture(event: str, **kwargs: Unpack[OptionalCaptureArgs]) -> Optional[str]:
7676
- `event name` to specify the event
7777
- We recommend using [verb] [noun], like `movie played` or `movie updated` to easily identify what your events mean later on.
7878
79-
Optionally you can submit
80-
- `distinct id` which uniquely identifies your user. This overrides any context-level ID, if set
81-
- `properties`, which can be a dict with any information you'd like to add
82-
- `groups`, which is a dict of group type -> group key mappings
79+
Capture takes a number of optional arguments, which are defined by the `OptionalCaptureArgs` type.
8380
8481
For example:
8582
```python
86-
posthog.capture('distinct id', 'opened app')
87-
posthog.capture('distinct id', 'movie played', {'movie_id': '123', 'category': 'romcom'})
83+
# Enter a new context (e.g. a request/response cycle, an instance of a background job, etc)
84+
with posthog.new_context():
85+
# Associate this context with some user, by distinct_id
86+
posthog.identify_context('some user')
8887
89-
posthog.capture('distinct id', 'purchase', groups={'company': 'id:5'})
88+
# Capture an event, associated with the context-level distinct ID ('some user')
89+
posthog.capture('movie started')
90+
91+
# Capture an event associated with some other user (overriding the context-level distinct ID)
92+
posthog.capture('movie joined', distinct_id='some-other-user')
93+
94+
# Capture an event with some properties
95+
posthog.capture('movie played', properties={'movie_id': '123', 'category': 'romcom'})
96+
97+
# Capture an event with some properties
98+
posthog.capture('purchase', properties={'product_id': '123', 'category': 'romcom'})
99+
# Capture an event with some associated group
100+
posthog.capture('purchase', groups={'company': 'id:5'})
101+
102+
# Adding a tag to the current context will cause it to appear on all subsequent events
103+
posthog.tag_context('some-tag', 'some-value')
104+
105+
posthog.capture('another-event') # Will be captured with `'some-tag': 'some-value'` in the properties dict
90106
```
91107
"""
92108

@@ -96,10 +112,14 @@ def capture(event: str, **kwargs: Unpack[OptionalCaptureArgs]) -> Optional[str]:
96112
def set(**kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
97113
"""
98114
Set properties on a user record.
99-
This will overwrite previous people property values, just like `identify`.
115+
This will overwrite previous people property values. Generally operates similar to `capture`, with
116+
distinct_id being an optional argument, defaulting to the current context's distinct ID.
100117
101-
A `set` call requires
102-
- `properties` with a dict with any key: value pairs
118+
If there is no context-level distinct ID, and no override distinct_id is passed, this function
119+
will do nothing.
120+
121+
Context tags are folded into $set properties, so tagging the current context and then calling `set` will
122+
cause those tags to be set on the user (unlike capture, which causes them to just be set on the event).
103123
104124
For example:
105125
```python
@@ -115,17 +135,9 @@ def set(**kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
115135
def set_once(**kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
116136
"""
117137
Set properties on a user record, only if they do not yet exist.
118-
This will not overwrite previous people property values, unlike `identify`.
119-
120-
A `set_once` call requires
121-
- `distinct id` which uniquely identifies your user
122-
- `properties` with a dict with any key: value pairs
138+
This will not overwrite previous people property values, unlike `set`.
123139
124-
For example:
125-
```python
126-
posthog.set_once('distinct id', {
127-
'referred_by': 'friend',
128-
})
140+
Otherwise, operates in an identical manner to `set`.
129141
```
130142
"""
131143
return _proxy("set_once", **kwargs)
@@ -146,7 +158,6 @@ def group_identify(
146158
A `group_identify` call requires
147159
- `group_type` type of your group
148160
- `group_key` unique identifier of the group
149-
- `properties` with a dict with any key: value pairs
150161
151162
For example:
152163
```python
@@ -176,11 +187,10 @@ def alias(
176187
):
177188
# type: (...) -> Optional[str]
178189
"""
179-
To marry up whatever a user does before they sign up or log in with what they do after you need to make an alias call. This will allow you to answer questions like "Which marketing channels leads to users churning after a month?" or "What do users do on our website before signing up?"
180-
181-
In a purely back-end implementation, this means whenever an anonymous user does something, you'll want to send a session ID ([Django](https://stackoverflow.com/questions/526179/in-django-how-can-i-find-out-the-request-session-sessionid-and-use-it-as-a-vari), [Flask](https://stackoverflow.com/questions/15156132/flask-login-how-to-get-session-id)) with the capture call. Then, when that users signs up, you want to do an alias call with the session ID and the newly created user ID.
182-
183-
The same concept applies for when a user logs in.
190+
To marry up whatever a user does before they sign up or log in with what they do after you need to make an alias call.
191+
This will allow you to answer questions like "Which marketing channels leads to users churning after a month?" or
192+
"What do users do on our website before signing up?". Particularly useful for associating user behaviour before and after
193+
they e.g. register, login, or perform some other identifying action.
184194
185195
An `alias` call requires
186196
- `previous distinct id` the unique ID of the user before
@@ -207,27 +217,26 @@ def capture_exception(
207217
**kwargs: Unpack[OptionalCaptureArgs],
208218
):
209219
"""
210-
capture_exception allows you to capture exceptions that happen in your code. This is useful for debugging and understanding what errors your users are encountering.
211-
This function never raises an exception, even if it fails to send the event.
220+
capture_exception allows you to capture exceptions that happen in your code.
221+
222+
Capture exception is idempotent - if it is called twice with the same exception instance, only a occurrence will be tracked in posthog.
223+
This is because, generally, contexts will cause exceptions to be captured automatically. However, to ensure you track an exception,
224+
if you catch and do not re-raise it, capturing it manually is recommended, unless you are certain it will have crossed a context
225+
boundary (e.g. by existing a `with posthog.new_context():` block already)
212226
213-
A `capture_exception` call does not require any fields, but we recommend sending:
214-
- `distinct id` which uniquely identifies your user for which this exception happens
227+
A `capture_exception` call does not require any fields, but we recommend passing an exception of some kind:
215228
- `exception` to specify the exception to capture. If not provided, the current exception is captured via `sys.exc_info()`
216229
217-
Optionally you can submit
218-
- `properties`, which can be a dict with any information you'd like to add
219-
- `groups`, which is a dict of group type -> group key mappings
220-
- remaining `kwargs` will be logged if `log_captured_exceptions` is enabled
230+
If the passed exception was raised and caught, the captured stack trace will consist of every frame between where the exception was raised
231+
and the point at which it is captured (the "traceback").
221232
222-
For example:
223-
```python
224-
try:
225-
1 / 0
226-
except Exception as e:
227-
posthog.capture_exception(e, 'my specific distinct id')
228-
posthog.capture_exception(distinct_id='my specific distinct id')
233+
If the passed exception was never raised, e.g. if you call `posthog.capture_exception(ValueError("Some Error"))`, the stack trace
234+
captured will be the full stack trace at the moment the exception was captured.
229235
230-
```
236+
Note that heavy use of contexts will lead to truncated stack traces, as the exception will be captured by the context entered most recently,
237+
which may not be the point you catch the exception for the final time in your code. It's recommended to use contexts sparingly, for this reason.
238+
239+
`capture_exception` takes the same set of optional arguments as `capture`.
231240
"""
232241

233242
return _proxy("capture_exception", exception=exception, **kwargs)

posthog/args.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,49 @@
99

1010

1111
class OptionalCaptureArgs(TypedDict):
12-
"""Optional arguments for the capture method."""
12+
"""Optional arguments for the capture method.
13+
14+
Args:
15+
distinct_id: Unique identifier for the person associated with this event. If not set, the context
16+
distinct_id is used, if available, otherwise a UUID is generated, and the event is marked
17+
as personless. Setting context-level distinct_id's is recommended.
18+
properties: Dictionary of properties to track with the event
19+
timestamp: When the event occurred (defaults to current time)
20+
uuid: Unique identifier for this specific event. If not provided, one is generated. The event
21+
UUID is returned, so you can correlate it with actions in your app (like showing users an
22+
error ID if you capture an exception).
23+
groups: Group identifiers to associate with this event (format: {group_type: group_key})
24+
send_feature_flags: Whether to include currently active feature flags in the event properties.
25+
Defaults to True
26+
disable_geoip: Whether to disable GeoIP lookup for this event. Defaults to False.
27+
"""
1328

1429
distinct_id: NotRequired[Optional[ID_TYPES]]
1530
properties: NotRequired[Optional[Dict[str, Any]]]
1631
timestamp: NotRequired[Optional[Union[datetime, str]]]
1732
uuid: NotRequired[Optional[str]]
1833
groups: NotRequired[Optional[Dict[str, str]]]
19-
send_feature_flags: NotRequired[bool]
20-
disable_geoip: NotRequired[Optional[bool]]
34+
send_feature_flags: NotRequired[
35+
Optional[bool]
36+
] # Optional so we can tell if the user is intentionally overriding a client setting or not
37+
disable_geoip: NotRequired[
38+
Optional[bool]
39+
] # As above, optional so we can tell if the user is intentionally overriding a client setting or not
2140

2241

2342
class OptionalSetArgs(TypedDict):
24-
"""Optional arguments for the set method."""
43+
"""Optional arguments for the set method.
44+
45+
Args:
46+
distinct_id: Unique identifier for the user to set properties on. If not set, the context
47+
distinct_id is used, if available, otherwise this function does nothing. Setting
48+
context-level distinct_id's is recommended.
49+
properties: Dictionary of properties to set on the person
50+
timestamp: When the properties were set (defaults to current time)
51+
uuid: Unique identifier for this operation. If not provided, one is generated. This
52+
UUID is returned, so you can correlate it with actions in your app.
53+
disable_geoip: Whether to disable GeoIP lookup for this operation. Defaults to False.
54+
"""
2555

2656
distinct_id: NotRequired[Optional[ID_TYPES]]
2757
properties: NotRequired[Optional[Dict[str, Any]]]

posthog/client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
from posthog.args import OptionalCaptureArgs, OptionalSetArgs, ID_TYPES, ExceptionArg
1414
from posthog.consumer import Consumer
15-
from posthog.scopes import new_context
1615
from posthog.exception_capture import ExceptionCapture
1716
from posthog.exception_utils import (
1817
exc_info_from_error,
@@ -32,10 +31,11 @@
3231
get,
3332
remote_config,
3433
)
35-
from posthog.scopes import (
34+
from posthog.contexts import (
3635
_get_current_context,
3736
get_context_distinct_id,
3837
get_context_session_id,
38+
new_context,
3939
)
4040
from posthog.types import (
4141
FeatureFlag,
@@ -415,7 +415,7 @@ def set(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
415415

416416
(distinct_id, personless) = get_identity_state(distinct_id)
417417

418-
if personless:
418+
if personless or not properties:
419419
return None # Personless set() does nothing
420420

421421
msg = {
@@ -440,7 +440,7 @@ def set_once(self, **kwargs: Unpack[OptionalSetArgs]) -> Optional[str]:
440440

441441
(distinct_id, personless) = get_identity_state(distinct_id)
442442

443-
if personless:
443+
if personless or not properties:
444444
return None # Personless set_once() does nothing
445445

446446
msg = {
File renamed without changes.

posthog/integrations/django.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from typing import TYPE_CHECKING, cast
2-
from posthog import scopes
2+
from posthog import contexts
33

44
if TYPE_CHECKING:
55
from django.http import HttpRequest, HttpResponse # noqa: F401
@@ -83,12 +83,12 @@ def extract_tags(self, request):
8383
# Extract session ID from X-POSTHOG-SESSION-ID header
8484
session_id = request.headers.get("X-POSTHOG-SESSION-ID")
8585
if session_id:
86-
scopes.set_context_session(session_id)
86+
contexts.set_context_session(session_id)
8787

8888
# Extract distinct ID from X-POSTHOG-DISTINCT-ID header or request user id
8989
distinct_id = request.headers.get("X-POSTHOG-DISTINCT-ID") or user_id
9090
if distinct_id:
91-
scopes.identify_context(distinct_id)
91+
contexts.identify_context(distinct_id)
9292

9393
# Extract user email
9494
if user_email:
@@ -153,8 +153,8 @@ def __call__(self, request):
153153
if self.request_filter and not self.request_filter(request):
154154
return self.get_response(request)
155155

156-
with scopes.new_context(self.capture_exceptions):
156+
with contexts.new_context(self.capture_exceptions):
157157
for k, v in self.extract_tags(request).items():
158-
scopes.tag(k, v)
158+
contexts.tag(k, v)
159159

160160
return self.get_response(request)

posthog/test/integrations/test_middleware.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
from posthog.scopes import new_context, get_context_session_id, get_context_distinct_id
1+
from posthog.contexts import (
2+
new_context,
3+
get_context_session_id,
4+
get_context_distinct_id,
5+
)
26
import unittest
37
from unittest.mock import Mock
48

posthog/test/test_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import unittest
33
from datetime import datetime
44
from uuid import uuid4
5-
from posthog.scopes import get_context_session_id, set_context_session, new_context
5+
from posthog.contexts import get_context_session_id, set_context_session, new_context
66

77
import mock
88
import six
@@ -1877,7 +1877,7 @@ def test_set_context_session_with_page_explicit_properties(self):
18771877

18781878
def test_set_context_session_override_in_capture(self):
18791879
"""Test that explicit session ID overrides context session ID in capture"""
1880-
from posthog.scopes import set_context_session, new_context
1880+
from posthog.contexts import set_context_session, new_context
18811881

18821882
with mock.patch("posthog.client.batch_post") as mock_post:
18831883
client = Client(FAKE_TEST_API_KEY, on_error=self.set_fail, sync_mode=True)
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import unittest
22
from unittest.mock import patch
33

4-
from posthog.scopes import (
4+
from posthog.contexts import (
55
get_tags,
66
new_context,
77
scoped,
@@ -13,7 +13,7 @@
1313
)
1414

1515

16-
class TestScopes(unittest.TestCase):
16+
class TestContexts(unittest.TestCase):
1717
def test_tag_and_get_tags(self):
1818
with new_context(fresh=True):
1919
tag("key1", "value1")

0 commit comments

Comments
 (0)