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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
21 changes: 20 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 12 additions & 1 deletion posthog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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]
Expand Down
47 changes: 42 additions & 5 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -648,6 +682,9 @@ def capture_exception(
stacklevel=2,
)

if distinct_id is None:
distinct_id = get_context_distinct_id()
Comment on lines +685 to +686
Copy link
Contributor

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?

Copy link
Contributor Author

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 capture under 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.


# 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:
Expand Down
12 changes: 7 additions & 5 deletions posthog/integrations/django.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
Expand Down Expand Up @@ -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()
Expand Down
Loading
Loading