1- import re
21from typing import TYPE_CHECKING , Any , Optional , cast
32
43from .. import contexts
76 _ExceptionCaptureMetadata ,
87 _capture_exception_with_metadata ,
98)
9+ from ._tracing_headers import _apply_tracing_headers , _resolve_capture_tracing_headers
1010
1111try :
1212 from asgiref .sync import iscoroutinefunction , markcoroutinefunction
@@ -27,35 +27,6 @@ def markcoroutinefunction(func):
2727 from typing import Callable , Dict , Any , Union , Awaitable # noqa: F401
2828
2929
30- _MAX_TRACING_HEADER_LENGTH = 1000
31- _TRACING_HEADER_CONTROL_CHARS_RE = re .compile (r"[\x00-\x1f\x7f-\x9f]" )
32-
33-
34- def _sanitize_tracing_header_value (value ) -> Optional [str ]:
35- """Return a safe tracing header value, or None if the value is invalid.
36-
37- Tracing headers come from user-controlled HTTP requests and are copied into event properties.
38- Match the PostHog app's header sanitization: accept strings only, remove C0/C1 control
39- characters, trim surrounding whitespace, cap length, and drop empty results.
40- """
41- if not isinstance (value , str ) or not value :
42- return None
43-
44- return (
45- _TRACING_HEADER_CONTROL_CHARS_RE .sub ("" , value ).strip ()[
46- :_MAX_TRACING_HEADER_LENGTH
47- ]
48- or None
49- )
50-
51-
52- def _get_sanitized_tracing_header (request , header_name ) -> Optional [str ]:
53- try :
54- return _sanitize_tracing_header_value (request .headers .get (header_name ))
55- except Exception :
56- return None
57-
58-
5930class PosthogContextMiddleware :
6031 """Middleware to automatically track Django requests.
6132
@@ -78,10 +49,13 @@ class PosthogContextMiddleware:
7849 - `POSTHOG_MW_REQUEST_FILTER`: a callback receiving the request. Returning `False`
7950 disables both enrichment and automatic exception capture for that request.
8051 - `POSTHOG_MW_EXTRA_PROPERTIES`: a callback returning additional event properties.
81- - `POSTHOG_MW_TRUST_TRACING_HEADERS`: accept the client-controlled PostHog identity
82- and session headers only when `True` (default: `False`). These values are analytics
83- context, never authentication or authorization. Authenticated Django user identity
84- is still available when headers are disabled.
52+ - `POSTHOG_MW_CAPTURE_TRACING_HEADERS`: read PostHog identity and session headers
53+ as request-scoped analytics context (default: `True`). Explicit capture IDs and
54+ event properties take precedence. These values are never authentication or
55+ authorization. Authenticated Django user identity remains the fallback when no
56+ usable header ID is available or header capture is disabled.
57+ - `POSTHOG_MW_TRUST_TRACING_HEADERS`: deprecated alias for
58+ `POSTHOG_MW_CAPTURE_TRACING_HEADERS`; the capture-named setting takes precedence.
8559 - `POSTHOG_MW_PROPERTIES_MAP`: an optional callback transforming request properties.
8660
8761 The old `POSTHOG_MW_EXTRA_TAGS` and `POSTHOG_MW_TAG_MAP` settings remain aliases;
@@ -159,11 +133,17 @@ def __init__(self, get_response):
159133 if capture_exceptions is None or isinstance (capture_exceptions , bool )
160134 else True
161135 )
136+ capture_tracing_headers = getattr (
137+ settings , "POSTHOG_MW_CAPTURE_TRACING_HEADERS" , None
138+ )
162139 trust_tracing_headers = getattr (
163- settings , "POSTHOG_MW_TRUST_TRACING_HEADERS" , False
140+ settings , "POSTHOG_MW_TRUST_TRACING_HEADERS" , None
164141 )
165- self .trust_tracing_headers = (
166- trust_tracing_headers if isinstance (trust_tracing_headers , bool ) else False
142+ self .capture_tracing_headers = _resolve_capture_tracing_headers (
143+ capture_tracing_headers ,
144+ trust_tracing_headers ,
145+ canonical_name = "POSTHOG_MW_CAPTURE_TRACING_HEADERS" ,
146+ deprecated_name = "POSTHOG_MW_TRUST_TRACING_HEADERS" ,
167147 )
168148
169149 if hasattr (settings , "POSTHOG_MW_CLIENT" ) and isinstance (
@@ -173,6 +153,15 @@ def __init__(self, get_response):
173153 else :
174154 self .client = None
175155
156+ @property
157+ def trust_tracing_headers (self ) -> bool :
158+ """Deprecated alias for ``capture_tracing_headers``."""
159+ return self .capture_tracing_headers
160+
161+ @trust_tracing_headers .setter
162+ def trust_tracing_headers (self , enabled : bool ) -> None :
163+ self .capture_tracing_headers = enabled
164+
176165 @property
177166 def extra_tags (self ):
178167 """Compatibility alias for ``extra_properties``."""
@@ -211,19 +200,11 @@ def _build_tags(self, request, user_id, user_email):
211200 """
212201 tags = {}
213202
214- # Client-controlled headers are analytics context, not authenticated identity.
215- session_id = None
216- distinct_id = user_id
217- if self .trust_tracing_headers :
218- session_id = _get_sanitized_tracing_header (request , "X-POSTHOG-SESSION-ID" )
219- distinct_id = (
220- _get_sanitized_tracing_header (request , "X-POSTHOG-DISTINCT-ID" )
221- or user_id
222- )
223- if session_id :
224- contexts .set_context_session (session_id )
225- if distinct_id :
226- contexts .identify_context (distinct_id )
203+ _apply_tracing_headers (
204+ request .headers .get ,
205+ self .capture_tracing_headers ,
206+ fallback_distinct_id = user_id ,
207+ )
227208
228209 # Extract user email
229210 if user_email :
0 commit comments