@@ -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