diff --git a/CHANGELOG.md b/CHANGELOG.md index cf018a90..c8d6db3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -## 5.0.0 +# 5.1.0 + +- feat: session and distinct ID's can now be associated with contexts, and are used as such +- feat: django http request middleware + +## 5.0.0 - 2025-06-16 - fix: removed deprecated sentry integration diff --git a/Makefile b/Makefile index e380ec8b..b6d867fa 100644 --- a/Makefile +++ b/Makefile @@ -35,4 +35,23 @@ release_analytics: e2e_test: .buildscripts/e2e.sh -.PHONY: test lint release e2e_test +prep_local: + rm -rf ../posthog-python-local + mkdir ../posthog-python-local + cp -r . ../posthog-python-local/ + cd ../posthog-python-local && rm -rf dist build posthoganalytics .git + cd ../posthog-python-local && mkdir posthoganalytics + cd ../posthog-python-local && cp -r posthog/* posthoganalytics/ + cd ../posthog-python-local && find ./posthoganalytics -type f -name "*.py" -exec sed -i.bak -e 's/from posthog /from posthoganalytics /g' {} \; + cd ../posthog-python-local && find ./posthoganalytics -type f -name "*.py" -exec sed -i.bak -e 's/from posthog\./from posthoganalytics\./g' {} \; + cd ../posthog-python-local && find ./posthoganalytics -name "*.bak" -delete + cd ../posthog-python-local && rm -rf posthog + cd ../posthog-python-local && sed -i.bak 's/from version import VERSION/from posthoganalytics.version import VERSION/' setup_analytics.py + cd ../posthog-python-local && rm setup_analytics.py.bak + cd ../posthog-python-local && sed -i.bak 's/"posthog"/"posthoganalytics"/' setup.py + cd ../posthog-python-local && rm setup.py.bak + cd ../posthog-python-local && python -c "import setup_analytics" 2>/dev/null || true + @echo "Local copy created at ../posthog-python-local" + @echo "Install with: pip install -e ../posthog-python-local" + +.PHONY: test lint release e2e_test prep_local diff --git a/posthog/__init__.py b/posthog/__init__.py index 84c1492c..f0f43403 100644 --- a/posthog/__init__.py +++ b/posthog/__init__.py @@ -4,7 +4,15 @@ from posthog.client import Client from posthog.exception_capture import Integrations # noqa: F401 -from posthog.scopes import clear_tags, get_tags, new_context, scoped, tag +from posthog.scopes import ( + clear_tags, + get_tags, + new_context, + scoped, + tag, + set_context_session, + identify_context, +) from posthog.types import FeatureFlag, FlagsAndPayloads from posthog.version import VERSION @@ -16,6 +24,9 @@ get_tags = get_tags clear_tags = clear_tags scoped = scoped +identify_context = identify_context +set_context_session = set_context_session + """Settings.""" api_key = None # type: Optional[str] diff --git a/posthog/client.py b/posthog/client.py index e166b1f6..7b581137 100644 --- a/posthog/client.py +++ b/posthog/client.py @@ -33,7 +33,11 @@ get, remote_config, ) -from posthog.scopes import get_tags +from posthog.scopes import ( + _get_current_context, + get_context_distinct_id, + get_context_session_id, +) from posthog.types import ( FeatureFlag, FeatureFlagResult, @@ -285,10 +289,17 @@ def identify( stacklevel=2, ) + if distinct_id is None: + distinct_id = get_context_distinct_id() + properties = properties or {} + require("distinct_id", distinct_id, ID_TYPES) require("properties", properties, dict) + if "$session_id" not in properties and get_context_session_id(): + properties["$session_id"] = get_context_session_id() + msg = { "timestamp": timestamp, "distinct_id": distinct_id, @@ -358,6 +369,9 @@ def get_flags_decision( """ Get feature flags decision, using either flags() or decide() API based on rollout. """ + + if distinct_id is None: + distinct_id = get_context_distinct_id() require("distinct_id", distinct_id, ID_TYPES) if disable_geoip is None: @@ -406,14 +420,22 @@ def capture( properties = {**(properties or {}), **system_context()} + if "$session_id" not in properties and get_context_session_id(): + properties["$session_id"] = get_context_session_id() + + if distinct_id is None: + distinct_id = get_context_distinct_id() + require("distinct_id", distinct_id, ID_TYPES) require("properties", properties, dict) require("event", event, string_types) - # Grab current context tags, if any exist - context_tags = get_tags() - if context_tags: - properties.update(context_tags) + current_context = _get_current_context() + if current_context: + context_tags = current_context.collect_tags() + # We want explicitly passed properties to override context tags + context_tags.update(properties) + properties = context_tags msg = { "properties": properties, @@ -480,6 +502,9 @@ def set( stacklevel=2, ) + if distinct_id is None: + distinct_id = get_context_distinct_id() + properties = properties or {} require("distinct_id", distinct_id, ID_TYPES) require("properties", properties, dict) @@ -510,6 +535,9 @@ def set_once( stacklevel=2, ) + if distinct_id is None: + distinct_id = get_context_distinct_id() + properties = properties or {} require("distinct_id", distinct_id, ID_TYPES) require("properties", properties, dict) @@ -581,6 +609,9 @@ def alias( stacklevel=2, ) + if distinct_id is None: + distinct_id = get_context_distinct_id() + require("previous_id", previous_id, ID_TYPES) require("distinct_id", distinct_id, ID_TYPES) @@ -613,6 +644,9 @@ def page( stacklevel=2, ) + if distinct_id is None: + distinct_id = get_context_distinct_id() + properties = properties or {} require("distinct_id", distinct_id, ID_TYPES) require("properties", properties, dict) @@ -648,6 +682,9 @@ def capture_exception( stacklevel=2, ) + if distinct_id is None: + distinct_id = get_context_distinct_id() + # this function shouldn't ever throw an error, so it logs exceptions instead of raising them. # this is important to ensure we don't unexpectedly re-raise exceptions in the user's code. try: diff --git a/posthog/integrations/django.py b/posthog/integrations/django.py index e6bbfce7..8f1abef9 100644 --- a/posthog/integrations/django.py +++ b/posthog/integrations/django.py @@ -10,8 +10,8 @@ class PosthogContextMiddleware: """Middleware to automatically track Django requests. This middleware wraps all calls with a posthog context. It attempts to extract the following from the request headers: - - Session ID as $session_id, (extracted from `X-POSTHOG-SESSION-ID`) - - Distinct ID as $distinct_id, (extracted from `X-POSTHOG-DISTINCT-ID`) + - Session ID, (extracted from `X-POSTHOG-SESSION-ID`) + - Distinct ID, (extracted from `X-POSTHOG-DISTINCT-ID`) - Request URL as $current_url - Request Method as $request_method @@ -26,7 +26,9 @@ class PosthogContextMiddleware: You can use the `POSTHOG_MW_TAG_MAP` function to remove any default tags you don't want to capture, or override them with your own values. Context tags are automatically included as properties on all events captured within a context, including exceptions. - See the context documentation for more information. + See the context documentation for more information. The extracted distinct ID and session ID, if found, are used to + associate all events captured in the middleware context with the same distinct ID and session as currently active on the + frontend. See the documentation for `set_context_session` and `identify_context` for more details. """ def __init__(self, get_response): @@ -79,12 +81,12 @@ def extract_tags(self, request): # Extract session ID from X-POSTHOG-SESSION-ID header session_id = request.headers.get("X-POSTHOG-SESSION-ID") if session_id: - tags["$session_id"] = session_id + scopes.set_context_session(session_id) # Extract distinct ID from X-POSTHOG-DISTINCT-ID header distinct_id = request.headers.get("X-POSTHOG-DISTINCT-ID") if distinct_id: - tags["$distinct_id"] = distinct_id + scopes.identify_context(distinct_id) # Extract current URL absolute_url = request.build_absolute_uri() diff --git a/posthog/scopes.py b/posthog/scopes.py index 732f0010..52a8f629 100644 --- a/posthog/scopes.py +++ b/posthog/scopes.py @@ -1,14 +1,66 @@ import contextvars from contextlib import contextmanager -from typing import Any, Callable, Dict, TypeVar, cast - -_context_stack: contextvars.ContextVar[list] = contextvars.ContextVar( - "posthog_context_stack", default=[{}] +from typing import Optional, Any, Callable, Dict, TypeVar, cast + + +class ContextScope: + def __init__( + self, + parent=None, + fresh: bool = False, + capture_exceptions: bool = True, + ): + self.parent = parent + self.fresh = fresh + self.capture_exceptions = capture_exceptions + self.session_id: Optional[str] = None + self.distinct_id: Optional[str] = None + self.tags: Dict[str, Any] = {} + + def set_session_id(self, session_id: str): + self.session_id = session_id + + def set_distinct_id(self, distinct_id: str): + self.distinct_id = distinct_id + + def add_tag(self, key: str, value: Any): + self.tags[key] = value + + def get_parent(self): + return self.parent + + def get_session_id(self) -> Optional[str]: + if self.session_id is not None: + return self.session_id + if self.parent is not None and not self.fresh: + return self.parent.get_session_id() + return None + + def get_distinct_id(self) -> Optional[str]: + if self.distinct_id is not None: + return self.distinct_id + if self.parent is not None and not self.fresh: + return self.parent.get_distinct_id() + return None + + def collect_tags(self) -> Dict[str, Any]: + tags = self.tags.copy() + if self.parent and not self.fresh: + # We want child tags to take precedence over parent tags, + # so we can't use a simple update here, instead collecting + # the parent tags and then updating with the child tags. + new_tags = self.parent.collect_tags() + tags.update(new_tags) + return tags + + +_context_stack: contextvars.ContextVar[Optional[ContextScope]] = contextvars.ContextVar( + "posthog_context_stack", default=None ) -def _get_current_context() -> Dict[str, Any]: - return _context_stack.get()[-1] +def _get_current_context() -> Optional[ContextScope]: + return _context_stack.get() @contextmanager @@ -20,8 +72,8 @@ def new_context(fresh=False, capture_exceptions=True): Args: fresh: Whether to start with a fresh context (default: False). - If False, inherits tags from parent context. - If True, starts with no tags. + 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). If True, captures exceptions and tags them with the context tags before propagating them. If False, exceptions will propagate without being tagged or captured. @@ -44,19 +96,18 @@ def new_context(fresh=False, capture_exceptions=True): """ from posthog import capture_exception - current_tags = _get_current_context().copy() - current_stack = _context_stack.get() - new_stack = current_stack + [{}] if fresh else current_stack + [current_tags] - token = _context_stack.set(new_stack) + current_context = _get_current_context() + new_context = ContextScope(current_context, fresh, capture_exceptions) + _context_stack.set(new_context) try: yield except Exception as e: - if capture_exceptions: + if new_context.capture_exceptions: capture_exception(e) raise finally: - _context_stack.reset(token) + _context_stack.set(new_context.get_parent()) def tag(key: str, value: Any) -> None: @@ -70,9 +121,13 @@ def tag(key: str, value: Any) -> None: Example: posthog.tag("user_id", "123") """ - _get_current_context()[key] = value + current_context = _get_current_context() + if current_context: + current_context.add_tag(key, value) +# NOTE: we should probably also remove this - there's no reason for the user to ever +# need to manually interact with the current tag set def get_tags() -> Dict[str, Any]: """ Get all tags from the current context. Note, modifying @@ -81,12 +136,75 @@ def get_tags() -> Dict[str, Any]: Returns: Dict of all tags in the current context """ - return _get_current_context().copy() + current_context = _get_current_context() + if current_context: + return current_context.collect_tags() + return {} +# NOTE: We should probably remove this function - the way to clear scope context +# is by entering a new, fresh context, rather than by clearing the tags or other +# scope data directly. def clear_tags() -> None: - """Clear all tags in the current context.""" - _get_current_context().clear() + """Clear all tags in the current context. Does not clear parent tags""" + current_context = _get_current_context() + if current_context: + current_context.tags.clear() + + +def identify_context(distinct_id: str) -> None: + """ + Identify the current context with a distinct ID, associating all events captured in this or + child contexts with the given distinct ID (unless identify_context is called again). This is overridden by + distinct id's passed directly to posthog.capture and related methods (identify, set etc). Entering a + fresh context will clear the context-level distinct ID. + + Args: + distinct_id: The distinct ID to associate with the current context and its children. + """ + current_context = _get_current_context() + if current_context: + current_context.set_distinct_id(distinct_id) + + +def set_context_session(session_id: str) -> None: + """ + Set the session ID for the current context, associating all events captured in this or + child contexts with the given session ID (unless set_context_session is called again). + Entering a fresh context will clear the context-level session ID. + + Args: + session_id: The session ID to associate with the current context and its children. See https://posthog.com/docs/data/sessions + """ + current_context = _get_current_context() + if current_context: + current_context.set_session_id(session_id) + + +def get_context_session_id() -> Optional[str]: + """ + Get the session ID for the current context. + + Returns: + The session ID if set, None otherwise + """ + current_context = _get_current_context() + if current_context: + return current_context.get_session_id() + return None + + +def get_context_distinct_id() -> Optional[str]: + """ + Get the distinct ID for the current context. + + Returns: + The distinct ID if set, None otherwise + """ + current_context = _get_current_context() + if current_context: + return current_context.get_distinct_id() + return None F = TypeVar("F", bound=Callable[..., Any]) diff --git a/posthog/test/integrations/test_middleware.py b/posthog/test/integrations/test_middleware.py index 034371a7..e7fcb54c 100644 --- a/posthog/test/integrations/test_middleware.py +++ b/posthog/test/integrations/test_middleware.py @@ -1,3 +1,4 @@ +from posthog.scopes import new_context, get_context_session_id, get_context_distinct_id import unittest from unittest.mock import Mock @@ -44,50 +45,55 @@ def create_middleware( return middleware def test_extract_tags_basic(self): - """Test basic tag extraction from request""" - middleware = self.create_middleware() - request = MockRequest( - headers={ - "X-POSTHOG-SESSION-ID": "session-123", - "X-POSTHOG-DISTINCT-ID": "user-456", - }, - method="POST", - path="/api/test", - host="example.com", - is_secure=True, - ) - - tags = middleware.extract_tags(request) - - self.assertEqual(tags["$session_id"], "session-123") - self.assertEqual(tags["$distinct_id"], "user-456") - self.assertEqual(tags["$current_url"], "https://example.com/api/test") - self.assertEqual(tags["$request_method"], "POST") + with new_context(): + """Test basic tag extraction from request""" + middleware = self.create_middleware() + request = MockRequest( + headers={ + "X-POSTHOG-SESSION-ID": "session-123", + "X-POSTHOG-DISTINCT-ID": "user-456", + }, + method="POST", + path="/api/test", + host="example.com", + is_secure=True, + ) + + tags = middleware.extract_tags(request) + + self.assertEqual(get_context_session_id(), "session-123") + self.assertEqual(get_context_distinct_id(), "user-456") + self.assertEqual(tags["$current_url"], "https://example.com/api/test") + self.assertEqual(tags["$request_method"], "POST") def test_extract_tags_missing_headers(self): """Test tag extraction when PostHog headers are missing""" - middleware = self.create_middleware() - request = MockRequest(headers={}, method="GET", path="/home") - tags = middleware.extract_tags(request) + with new_context(): + middleware = self.create_middleware() + request = MockRequest(headers={}, method="GET", path="/home") - self.assertNotIn("$session_id", tags) - self.assertNotIn("$distinct_id", tags) - self.assertEqual(tags["$current_url"], "http://example.com/home") - self.assertEqual(tags["$request_method"], "GET") + tags = middleware.extract_tags(request) + + self.assertIsNone(get_context_session_id()) + self.assertIsNone(get_context_distinct_id()) + self.assertEqual(tags["$current_url"], "http://example.com/home") + self.assertEqual(tags["$request_method"], "GET") def test_extract_tags_partial_headers(self): """Test tag extraction with only some PostHog headers present""" - middleware = self.create_middleware() - request = MockRequest( - headers={"X-POSTHOG-SESSION-ID": "session-only"}, method="PUT" - ) - tags = middleware.extract_tags(request) + with new_context(): + middleware = self.create_middleware() + request = MockRequest( + headers={"X-POSTHOG-SESSION-ID": "session-only"}, method="PUT" + ) + + tags = middleware.extract_tags(request) - self.assertEqual(tags["$session_id"], "session-only") - self.assertNotIn("$distinct_id", tags) - self.assertEqual(tags["$request_method"], "PUT") + self.assertEqual(get_context_session_id(), "session-only") + self.assertIsNone(get_context_distinct_id()) + self.assertEqual(tags["$request_method"], "PUT") def test_extract_tags_with_extra_tags(self): """Test tag extraction with extra_tags function""" @@ -95,60 +101,41 @@ def test_extract_tags_with_extra_tags(self): def extra_tags_func(request): return {"custom_tag": "custom_value", "user_id": "789"} - middleware = self.create_middleware(extra_tags=extra_tags_func) - request = MockRequest( - headers={"X-POSTHOG-SESSION-ID": "session-123"}, method="GET" - ) + with new_context(): + middleware = self.create_middleware(extra_tags=extra_tags_func) + request = MockRequest( + headers={"X-POSTHOG-SESSION-ID": "session-123"}, method="GET" + ) - tags = middleware.extract_tags(request) + tags = middleware.extract_tags(request) - self.assertEqual(tags["$session_id"], "session-123") - self.assertEqual(tags["custom_tag"], "custom_value") - self.assertEqual(tags["user_id"], "789") + self.assertEqual(get_context_session_id(), "session-123") + self.assertEqual(tags["custom_tag"], "custom_value") + self.assertEqual(tags["user_id"], "789") def test_extract_tags_with_tag_map(self): """Test tag extraction with tag_map function""" - def tag_map_func(tags): - # Remove session_id and add a mapped version - if "$session_id" in tags: - tags["mapped_session"] = f"mapped_{tags['$session_id']}" - del tags["$session_id"] - return tags - - middleware = self.create_middleware(tag_map=tag_map_func) - request = MockRequest( - headers={"X-POSTHOG-SESSION-ID": "session-123"}, method="GET" - ) - - tags = middleware.extract_tags(request) - - self.assertNotIn("$session_id", tags) - self.assertEqual(tags["mapped_session"], "mapped_session-123") - - def test_extract_tags_with_both_extra_and_map(self): - """Test tag extraction with both extra_tags and tag_map""" - def extra_tags_func(request): - return {"extra": "value"} + return {"custom_tag": "custom_value", "user_id": "789"} def tag_map_func(tags): - tags["modified"] = True + if "custom_tag" in tags: + tags["mapped_custom_tag"] = f"mapped_{tags['custom_tag']}" + del tags["custom_tag"] return tags - middleware = self.create_middleware( - extra_tags=extra_tags_func, tag_map=tag_map_func - ) - request = MockRequest( - headers={"X-POSTHOG-DISTINCT-ID": "user-123"}, method="DELETE" - ) + with new_context(): + middleware = self.create_middleware( + tag_map=tag_map_func, extra_tags=extra_tags_func + ) + request = MockRequest( + headers={"X-POSTHOG-SESSION-ID": "session-123"}, method="GET" + ) - tags = middleware.extract_tags(request) + tags = middleware.extract_tags(request) - self.assertEqual(tags["$distinct_id"], "user-123") - self.assertEqual(tags["extra"], "value") - self.assertEqual(tags["modified"], True) - self.assertEqual(tags["$request_method"], "DELETE") + self.assertEqual(tags["mapped_custom_tag"], "mapped_custom_value") def test_extract_tags_extra_tags_returns_none(self): """Test tag extraction when extra_tags returns None""" diff --git a/posthog/test/test_scopes.py b/posthog/test/test_scopes.py index e14bb87d..079552e2 100644 --- a/posthog/test/test_scopes.py +++ b/posthog/test/test_scopes.py @@ -1,7 +1,17 @@ import unittest from unittest.mock import patch -from posthog.scopes import clear_tags, get_tags, new_context, scoped, tag +from posthog.scopes import ( + clear_tags, + get_tags, + new_context, + scoped, + tag, + identify_context, + set_context_session, + get_context_session_id, + get_context_distinct_id, +) class TestScopes(unittest.TestCase): @@ -10,60 +20,64 @@ def setUp(self): clear_tags() def test_tag_and_get_tags(self): - tag("key1", "value1") - tag("key2", 2) + with new_context(fresh=True): + tag("key1", "value1") + tag("key2", 2) - tags = get_tags() - assert tags["key1"] == "value1" - assert tags["key2"] == 2 + tags = get_tags() + assert tags["key1"] == "value1" + assert tags["key2"] == 2 def test_clear_tags(self): - tag("key1", "value1") - assert get_tags()["key1"] == "value1" + with new_context(fresh=True): + tag("key1", "value1") + assert get_tags()["key1"] == "value1" - clear_tags() - assert get_tags() == {} + clear_tags() + assert get_tags() == {} def test_new_context_isolation(self): - # Set tag in outer context - tag("outer", "value") - with new_context(fresh=True): - # Inner context should start empty - assert get_tags() == {} + # Set tag in outer context + tag("outer", "value") + + with new_context(fresh=True): + # Inner context should start empty + assert get_tags() == {} - # Set tag in inner context - tag("inner", "value") - assert get_tags()["inner"] == "value" + # Set tag in inner context + tag("inner", "value") + assert get_tags()["inner"] == "value" - # Outer tag should not be visible - self.assertNotIn("outer", get_tags()) + # Outer tag should not be visible + self.assertNotIn("outer", get_tags()) - with new_context(fresh=False): - # Inner context should start empty - assert get_tags() == {"outer": "value"} + with new_context(fresh=False): + # Inner context should inherit outer tag + assert get_tags() == {"outer": "value"} - # After exiting context, inner tag should be gone - self.assertNotIn("inner", get_tags()) + # After exiting context, inner tag should be gone + self.assertNotIn("inner", get_tags()) - # Outer tag should still be there - assert get_tags()["outer"] == "value" + # Outer tag should still be there + assert get_tags()["outer"] == "value" def test_nested_contexts(self): - tag("level1", "value1") - with new_context(fresh=True): - tag("level2", "value2") + tag("level1", "value1") with new_context(fresh=True): - tag("level3", "value3") - assert get_tags() == {"level3": "value3"} + tag("level2", "value2") - # Back to level 2 - assert get_tags() == {"level2": "value2"} + with new_context(fresh=True): + tag("level3", "value3") + assert get_tags() == {"level3": "value3"} - # Back to level 1 - assert get_tags() == {"level1": "value1"} + # Back to level 2 + assert get_tags() == {"level2": "value2"} + + # Back to level 1 + assert get_tags() == {"level1": "value1"} @patch("posthog.capture_exception") def test_scoped_decorator_success(self, mock_capture): @@ -122,17 +136,85 @@ def check_context_on_capture(exception, **kwargs): mock_capture.side_effect = check_context_on_capture # Set up outer context - tag("outer_context", "outer_value") + with new_context(): + tag("outer_context", "outer_value") + + try: + with new_context(): + tag("inner_context", "inner_value") + raise test_exception + except RuntimeError: + pass # Expected exception - try: - with new_context(): - tag("inner_context", "inner_value") - raise test_exception - except RuntimeError: - pass # Expected exception + # Outer context should still be intact + assert get_tags()["outer_context"] == "outer_value" # Verify capture_exception was called mock_capture.assert_called_once_with(test_exception) - # Outer context should still be intact - assert get_tags()["outer_context"] == "outer_value" + def test_identify_context(self): + with new_context(fresh=True): + # Initially no distinct ID + assert get_context_distinct_id() is None + + # Set distinct ID + identify_context("user123") + assert get_context_distinct_id() == "user123" + + def test_set_context_session(self): + with new_context(fresh=True): + # Initially no session ID + assert get_context_session_id() is None + + # Set session ID + set_context_session("session456") + assert get_context_session_id() == "session456" + + def test_context_inheritance_fresh_context(self): + with new_context(fresh=True): + identify_context("user123") + set_context_session("session456") + + with new_context(fresh=True): + # Fresh context should not inherit + assert get_context_distinct_id() is None + assert get_context_session_id() is None + + # Original context should still have values + assert get_context_distinct_id() == "user123" + assert get_context_session_id() == "session456" + + def test_context_inheritance_non_fresh_context(self): + with new_context(fresh=True): + identify_context("user123") + set_context_session("session456") + + with new_context(fresh=False): + # Non-fresh context should inherit + assert get_context_distinct_id() == "user123" + assert get_context_session_id() == "session456" + + # Override in child context + identify_context("user789") + set_context_session("session999") + assert get_context_distinct_id() == "user789" + assert get_context_session_id() == "session999" + + # Original context should still have original values + assert get_context_distinct_id() == "user123" + assert get_context_session_id() == "session456" + + def test_scoped_decorator_with_context_ids(self): + @scoped() + def function_with_context(): + identify_context("user456") + set_context_session("session789") + return get_context_distinct_id(), get_context_session_id() + + distinct_id, session_id = function_with_context() + assert distinct_id == "user456" + assert session_id == "session789" + + # Context should be cleared after function execution + assert get_context_distinct_id() is None + assert get_context_session_id() is None diff --git a/posthog/version.py b/posthog/version.py index a4bfa51b..ab0c9f5d 100644 --- a/posthog/version.py +++ b/posthog/version.py @@ -1,4 +1,4 @@ -VERSION = "5.0.0" +VERSION = "5.1.0" if __name__ == "__main__": print(VERSION, end="") # noqa: T201 diff --git a/pyproject.toml b/pyproject.toml index 8f7d6312..344af296 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,6 +87,7 @@ packages = [ "posthog.ai.gemini", "posthog.test", "posthog.exception_integrations", + "posthog.integrations", ] [tool.setuptools.dynamic]