Skip to content

Commit d125873

Browse files
committed
fix(django): restore process_exception to capture view exceptions
Restores the process_exception method that was removed in v6.7.5 (PR #328), which broke exception capture from Django views and downstream middleware. Django converts view exceptions into responses before they propagate through the middleware stack's __call__ method, so the context manager's exception handler never sees them. Django provides these exceptions via the process_exception hook instead. Changes: - Add process_exception method to capture exceptions from views and downstream middleware with proper request context and tags - Add tests verifying process_exception behavior and settings (capture_exceptions, request_filter) - Update version to 6.7.11 - Update CHANGELOG to accurately reflect the fix Context: - v6.7.4 and earlier: no process_exception - exceptions not captured (#286) - v6.7.4 fix: added process_exception - worked correctly - v6.7.5 (PR #328): removed process_exception when adding async - broke again (#329) - v6.7.10 (PR #348): attempted fix but didn't restore process_exception - still broken - v6.7.11 (this fix): restores process_exception - fixes #329 Fixes #329
1 parent 13184e2 commit d125873

File tree

4 files changed

+89
-2
lines changed

4 files changed

+89
-2
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
# 6.7.11 - 2025-10-24
2+
3+
- fix(django): Restore process_exception method to capture view and downstream middleware exceptions (fixes #329)
4+
15
# 6.7.10 - 2025-10-24
26

37
- fix(django): Make middleware truly hybrid - compatible with both sync (WSGI) and async (ASGI) Django stacks without breaking sync-only deployments
4-
- fix(django): Exception capture works correctly via context manager (addresses #329)
8+
- Note: v6.7.10 did not fully fix exception capture - see v6.7.11
59

610
# 6.7.9 - 2025-10-22
711

posthog/integrations/django.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,33 @@ async def __acall__(self, request):
220220
contexts.tag(k, v)
221221

222222
return await self.get_response(request)
223+
224+
def process_exception(self, request, exception):
225+
# type: (HttpRequest, Exception) -> None
226+
"""
227+
Process exceptions from views and downstream middleware.
228+
229+
Django converts view exceptions into responses before they propagate through
230+
the middleware stack, so the context manager in __call__ never sees them.
231+
This method is called by Django's exception handling to provide middleware
232+
access to exceptions.
233+
234+
Note: Django's process_exception is always synchronous, even for async views.
235+
"""
236+
if self.request_filter and not self.request_filter(request):
237+
return
238+
239+
if not self.capture_exceptions:
240+
return
241+
242+
# Create context with request tags for exception capture
243+
with contexts.new_context(self.capture_exceptions, client=self.client):
244+
for k, v in self.extract_tags(request).items():
245+
contexts.tag(k, v)
246+
247+
# Capture exception with context tags
248+
if self.client:
249+
self.client.capture_exception(exception)
250+
else:
251+
from posthog import capture_exception
252+
capture_exception(exception)

posthog/test/integrations/test_middleware.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,59 @@ def raise_exception(request):
276276
self.assertIsInstance(captured_exception, ValueError)
277277
self.assertEqual(str(captured_exception), "Test exception")
278278

279+
def test_process_exception_captures_view_exceptions(self):
280+
"""Test that process_exception captures exceptions from views"""
281+
mock_client = Mock()
282+
283+
get_response = Mock(return_value=Mock())
284+
middleware = PosthogContextMiddleware(get_response)
285+
middleware.client = mock_client
286+
287+
request = MockRequest()
288+
exception = ValueError("View exception")
289+
290+
# Call process_exception directly (Django does this)
291+
middleware.process_exception(request, exception)
292+
293+
# Verify exception was captured
294+
mock_client.capture_exception.assert_called_once_with(exception)
295+
296+
def test_process_exception_respects_capture_exceptions_setting(self):
297+
"""Test that process_exception respects POSTHOG_MW_CAPTURE_EXCEPTIONS=False"""
298+
mock_client = Mock()
299+
300+
get_response = Mock(return_value=Mock())
301+
middleware = PosthogContextMiddleware(get_response)
302+
middleware.client = mock_client
303+
middleware.capture_exceptions = False
304+
305+
request = MockRequest()
306+
exception = ValueError("View exception")
307+
308+
# Call process_exception
309+
middleware.process_exception(request, exception)
310+
311+
# Should NOT capture when disabled
312+
mock_client.capture_exception.assert_not_called()
313+
314+
def test_process_exception_respects_request_filter(self):
315+
"""Test that process_exception respects request filter"""
316+
mock_client = Mock()
317+
318+
get_response = Mock(return_value=Mock())
319+
middleware = PosthogContextMiddleware(get_response)
320+
middleware.client = mock_client
321+
middleware.request_filter = lambda req: False # Filter out all requests
322+
323+
request = MockRequest()
324+
exception = ValueError("View exception")
325+
326+
# Call process_exception
327+
middleware.process_exception(request, exception)
328+
329+
# Should NOT capture when filtered
330+
mock_client.capture_exception.assert_not_called()
331+
279332

280333
class TestPosthogContextMiddlewareAsync(unittest.TestCase):
281334
"""Test asynchronous middleware behavior"""

posthog/version.py

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

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

0 commit comments

Comments
 (0)