Skip to content

Commit f17ebfa

Browse files
authored
feat: more django context (#252)
1 parent 800527d commit f17ebfa

File tree

5 files changed

+104
-16
lines changed

5 files changed

+104
-16
lines changed

CHANGELOG.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
## 4.5.0- 2025-06-09
1+
## 4.6.0 - 2025-06-09
2+
3+
- feat: add additional user and request context to captured exceptions via the Django integration
4+
- feat: Add `setup()` function to initialise default client
5+
6+
## 4.5.0 - 2025-06-09
27

38
- feat: add before_send callback (#249)
49

@@ -17,11 +22,13 @@
1722
## 4.3.2 - 2025-06-06
1823

1924
1. Add context management:
20-
- New context manager with `posthog.new_context()`
21-
- Tag functions: `posthog.tag()`, `posthog.get_tags()`, `posthog.clear_tags()`
22-
- Function decorator:
23-
- `@posthog.scoped` - Creates context and captures exceptions thrown within the function
24-
- Automatic deduplication of exceptions to ensure each exception is only captured once
25+
26+
- New context manager with `posthog.new_context()`
27+
- Tag functions: `posthog.tag()`, `posthog.get_tags()`, `posthog.clear_tags()`
28+
- Function decorator:
29+
- `@posthog.scoped` - Creates context and captures exceptions thrown within the function
30+
- Automatic deduplication of exceptions to ensure each exception is only captured once
31+
2532
2. fix: feature flag request use geoip_disable (#235)
2633
3. chore: pin actions versions (#210)
2734
4. fix: opinionated setup and clean fn fix (#240)

posthog/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -580,8 +580,7 @@ def shutdown():
580580
_proxy("join")
581581

582582

583-
def _proxy(method, *args, **kwargs):
584-
"""Create an analytics client if one doesn't exist and send to it."""
583+
def setup():
585584
global default_client
586585
if not default_client:
587586
default_client = Client(
@@ -610,6 +609,11 @@ def _proxy(method, *args, **kwargs):
610609
default_client.disabled = disabled
611610
default_client.debug = debug
612611

612+
613+
def _proxy(method, *args, **kwargs):
614+
"""Create an analytics client if one doesn't exist and send to it."""
615+
setup()
616+
613617
fn = getattr(default_client, method)
614618
return fn(*args, **kwargs)
615619

posthog/exception_integrations/django.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ def extract_person_data(self):
6767
headers = self.headers()
6868

6969
# Extract traceparent and tracestate headers
70-
traceparent = headers.get("traceparent")
71-
tracestate = headers.get("tracestate")
70+
traceparent = headers.get("Traceparent")
71+
tracestate = headers.get("Tracestate")
7272

7373
# Extract the distinct_id from tracestate
7474
distinct_id = None
@@ -80,12 +80,38 @@ def extract_person_data(self):
8080
distinct_id = match.group(1)
8181

8282
return {
83+
**self.user(),
8384
"distinct_id": distinct_id,
8485
"ip": headers.get("X-Forwarded-For"),
8586
"user_agent": headers.get("User-Agent"),
8687
"traceparent": traceparent,
88+
"$request_path": self.request.path,
8789
}
8890

91+
def user(self):
92+
user_data: dict[str, str] = {}
93+
94+
user = getattr(self.request, "user", None)
95+
96+
if user is None or not user.is_authenticated:
97+
return user_data
98+
99+
try:
100+
user_id = str(user.pk)
101+
if user_id:
102+
user_data.setdefault("$user_id", user_id)
103+
except Exception:
104+
pass
105+
106+
try:
107+
email = str(user.email)
108+
if email:
109+
user_data.setdefault("email", email)
110+
except Exception:
111+
pass
112+
113+
return user_data
114+
89115
def headers(self):
90116
# type: () -> Dict[str, str]
91117
return dict(self.request.headers)

posthog/test/exception_integrations/test_django.py

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
11
from posthog.exception_integrations.django import DjangoRequestExtractor
2+
from django.test import RequestFactory
3+
from django.conf import settings
4+
from django.core.management import call_command
5+
import django
26

37
DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
48

9+
# setup a test app
10+
if not settings.configured:
11+
settings.configure(
12+
SECRET_KEY="test",
13+
DEFAULT_CHARSET="utf-8",
14+
INSTALLED_APPS=[
15+
"django.contrib.auth",
16+
"django.contrib.contenttypes",
17+
],
18+
DATABASES={
19+
"default": {
20+
"ENGINE": "django.db.backends.sqlite3",
21+
"NAME": ":memory:",
22+
}
23+
},
24+
)
25+
django.setup()
26+
27+
call_command("migrate", verbosity=0, interactive=False)
28+
529

630
def mock_request_factory(override_headers):
7-
class Request:
8-
META = {}
9-
# TRICKY: Actual django request dict object has case insensitive matching, and strips http from the names
10-
headers = {
31+
factory = RequestFactory(
32+
headers={
1133
"User-Agent": DEFAULT_USER_AGENT,
1234
"Referrer": "http://example.com",
1335
"X-Forwarded-For": "193.4.5.12",
1436
**(override_headers or {}),
1537
}
38+
)
1639

17-
return Request()
40+
request = factory.get("/api/endpoint")
41+
return request
1842

1943

2044
def test_request_extractor_with_no_trace():
@@ -25,19 +49,22 @@ def test_request_extractor_with_no_trace():
2549
"user_agent": DEFAULT_USER_AGENT,
2650
"traceparent": None,
2751
"distinct_id": None,
52+
"$request_path": "/api/endpoint",
2853
}
2954

3055

3156
def test_request_extractor_with_trace():
3257
request = mock_request_factory(
3358
{"traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"}
3459
)
60+
3561
extractor = DjangoRequestExtractor(request)
3662
assert extractor.extract_person_data() == {
3763
"ip": "193.4.5.12",
3864
"user_agent": DEFAULT_USER_AGENT,
3965
"traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
4066
"distinct_id": None,
67+
"$request_path": "/api/endpoint",
4168
}
4269

4370

@@ -54,6 +81,7 @@ def test_request_extractor_with_tracestate():
5481
"user_agent": DEFAULT_USER_AGENT,
5582
"traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
5683
"distinct_id": "1234",
84+
"$request_path": "/api/endpoint",
5785
}
5886

5987

@@ -67,4 +95,27 @@ def test_request_extractor_with_complicated_tracestate():
6795
"user_agent": DEFAULT_USER_AGENT,
6896
"traceparent": None,
6997
"distinct_id": "alohaMountainsXUYZ",
98+
"$request_path": "/api/endpoint",
99+
}
100+
101+
102+
def test_request_extractor_with_request_user():
103+
from django.contrib.auth.models import User
104+
105+
user = User.objects.create_user(
106+
username="test", email="[email protected]", password="top_secret"
107+
)
108+
109+
request = mock_request_factory(None)
110+
request.user = user
111+
112+
extractor = DjangoRequestExtractor(request)
113+
assert extractor.extract_person_data() == {
114+
"ip": "193.4.5.12",
115+
"user_agent": DEFAULT_USER_AGENT,
116+
"traceparent": None,
117+
"distinct_id": None,
118+
"$request_path": "/api/endpoint",
119+
"email": "[email protected]",
120+
"$user_id": "1",
70121
}

posthog/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
VERSION = "4.5.0"
1+
VERSION = "4.6.0"
22

33
if __name__ == "__main__":
44
print(VERSION, end="") # noqa: T201

0 commit comments

Comments
 (0)