-
Notifications
You must be signed in to change notification settings - Fork 52
feat: session and identity integrate with context now #264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
a87a0cc
82d500b
6701552
40d5596
ee9ca0d
b42351e
1c01c98
282d681
8a42006
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 Self, Optional, Any, Callable, Dict, TypeVar, cast | ||
|
|
||
|
|
||
| class ContextScope: | ||
| def __init__( | ||
| self, | ||
| parent: Optional[Self], | ||
| 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) -> Optional[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 | ||
|
|
@@ -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,73 @@ 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we just remove this and the above? Feels like the tagging stuff is still in flux and I'm not sure anyone is fully relying on it yet. Maybe fine to wait given we're doing a major version bump soon
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was gonna wait for the major version bump, yeah |
||
| # 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_scope is called again). This is overridden by | ||
| distinct id's passed directly to posthog.capture and related methods (identify, set etc). | ||
oliverb123 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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_scope_session is called again). | ||
oliverb123 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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]) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the session data not being added here like it is in some of the earlier methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Capture exception doesn't add the session id because it calls
captureunder the hood, which does. It only bothers grabbing the distinct id because it's is unique in that it auto-generates a distinct id if one isn't set, rather than throwing. To prevent that auto-generation, we have to try and grab a context distinct id up front.