Skip to content

Commit 8e5444f

Browse files
committed
refactor(django): DRY improvements to async middleware
Extract common logic into helper methods to eliminate duplication: - Add _build_tags() helper used by both extract_tags and aextract_tags Eliminates ~50 lines of duplicated tag extraction logic - Add _resolve_user_details() helper for user info extraction Centralizes user ID/email extraction logic - Use defensive getattr() instead of try/except More readable and explicit about what can be None - Handle callable is_authenticated for legacy Django Supports Django versions where is_authenticated was a method Benefits: - Single source of truth for tag extraction logic - Easier maintenance (change once, applies to sync and async) - More robust user detail extraction - All 24 tests still pass Inspired by code review feedback while maintaining Django conventions (aextract_* naming pattern).
1 parent 15963d0 commit 8e5444f

File tree

1 file changed

+56
-85
lines changed

1 file changed

+56
-85
lines changed

posthog/integrations/django.py

Lines changed: 56 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,18 @@ def __init__(self, get_response):
112112

113113
def extract_tags(self, request):
114114
# type: (HttpRequest) -> Dict[str, Any]
115-
tags = {}
115+
"""Extract tags from request in sync context."""
116+
user_id, user_email = self.extract_request_user(request)
117+
return self._build_tags(request, user_id, user_email)
118+
119+
def _build_tags(self, request, user_id, user_email):
120+
# type: (HttpRequest, Optional[str], Optional[str]) -> Dict[str, Any]
121+
"""
122+
Build tags dict from request and user info.
116123
117-
(user_id, user_email) = self.extract_request_user(request)
124+
Centralized tag extraction logic used by both sync and async paths.
125+
"""
126+
tags = {}
118127

119128
# Extract session ID from X-POSTHOG-SESSION-ID header
120129
session_id = request.headers.get("X-POSTHOG-SESSION-ID")
@@ -166,23 +175,10 @@ def extract_tags(self, request):
166175
return tags
167176

168177
def extract_request_user(self, request):
169-
user_id = None
170-
email = None
171-
178+
# type: (HttpRequest) -> tuple[Optional[str], Optional[str]]
179+
"""Extract user ID and email from request in sync context."""
172180
user = getattr(request, "user", None)
173-
174-
if user and getattr(user, "is_authenticated", False):
175-
try:
176-
user_id = str(user.pk)
177-
except Exception:
178-
pass
179-
180-
try:
181-
email = str(user.email)
182-
except Exception:
183-
pass
184-
185-
return user_id, email
181+
return self._resolve_user_details(user)
186182

187183
async def aextract_tags(self, request):
188184
# type: (HttpRequest) -> Dict[str, Any]
@@ -194,60 +190,11 @@ async def aextract_tags(self, request):
194190
195191
Follows Django's naming convention for async methods (auser, asave, etc.).
196192
"""
197-
tags = {}
198-
199-
(user_id, user_email) = await self.aextract_request_user(request)
200-
201-
# Extract session ID from X-POSTHOG-SESSION-ID header
202-
session_id = request.headers.get("X-POSTHOG-SESSION-ID")
203-
if session_id:
204-
contexts.set_context_session(session_id)
205-
206-
# Extract distinct ID from X-POSTHOG-DISTINCT-ID header or request user id
207-
distinct_id = request.headers.get("X-POSTHOG-DISTINCT-ID") or user_id
208-
if distinct_id:
209-
contexts.identify_context(distinct_id)
210-
211-
# Extract user email
212-
if user_email:
213-
tags["email"] = user_email
214-
215-
# Extract current URL
216-
absolute_url = request.build_absolute_uri()
217-
if absolute_url:
218-
tags["$current_url"] = absolute_url
219-
220-
# Extract request method
221-
if request.method:
222-
tags["$request_method"] = request.method
223-
224-
# Extract request path
225-
if request.path:
226-
tags["$request_path"] = request.path
227-
228-
# Extract IP address
229-
ip_address = request.headers.get("X-Forwarded-For")
230-
if ip_address:
231-
tags["$ip_address"] = ip_address
232-
233-
# Extract user agent
234-
user_agent = request.headers.get("User-Agent")
235-
if user_agent:
236-
tags["$user_agent"] = user_agent
237-
238-
# Apply extra tags if configured
239-
if self.extra_tags:
240-
extra = self.extra_tags(request)
241-
if extra:
242-
tags.update(extra)
243-
244-
# Apply tag mapping if configured
245-
if self.tag_map:
246-
tags = self.tag_map(tags)
247-
248-
return tags
193+
user_id, user_email = await self.aextract_request_user(request)
194+
return self._build_tags(request, user_id, user_email)
249195

250196
async def aextract_request_user(self, request):
197+
# type: (HttpRequest) -> tuple[Optional[str], Optional[str]]
251198
"""
252199
Async version of extract_request_user for use in async request handling.
253200
@@ -256,26 +203,50 @@ async def aextract_request_user(self, request):
256203
257204
Follows Django's naming convention for async methods (auser, asave, etc.).
258205
"""
206+
auser = getattr(request, "auser", None)
207+
if callable(auser):
208+
try:
209+
user = await auser()
210+
return self._resolve_user_details(user)
211+
except Exception:
212+
# If auser() fails, return empty - don't break the request
213+
# Real errors (permissions, broken auth) will be logged by Django
214+
return None, None
215+
216+
# Fallback for test requests without auser
217+
return None, None
218+
219+
def _resolve_user_details(self, user):
220+
# type: (Any) -> tuple[Optional[str], Optional[str]]
221+
"""
222+
Extract user ID and email from a user object.
223+
224+
Handles both authenticated and unauthenticated users, as well as
225+
legacy Django where is_authenticated was a method.
226+
"""
259227
user_id = None
260228
email = None
261229

262-
# In async context, use auser() instead of user attribute
263-
if hasattr(request, "auser"):
264-
user = await request.auser()
265-
else:
266-
# Fallback for non-Django or test requests
267-
user = getattr(request, "user", None)
230+
if user is None:
231+
return user_id, email
268232

269-
if user and getattr(user, "is_authenticated", False):
270-
try:
271-
user_id = str(user.pk)
272-
except Exception:
273-
pass
233+
# Handle is_authenticated (property in modern Django, method in legacy)
234+
is_authenticated = getattr(user, "is_authenticated", False)
235+
if callable(is_authenticated):
236+
is_authenticated = is_authenticated()
274237

275-
try:
276-
email = str(user.email)
277-
except Exception:
278-
pass
238+
if not is_authenticated:
239+
return user_id, email
240+
241+
# Extract user primary key
242+
user_pk = getattr(user, "pk", None)
243+
if user_pk is not None:
244+
user_id = str(user_pk)
245+
246+
# Extract user email
247+
user_email = getattr(user, "email", None)
248+
if user_email:
249+
email = str(user_email)
279250

280251
return user_id, email
281252

0 commit comments

Comments
 (0)