Skip to content

Commit 8d0d253

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 8d0d253

File tree

4 files changed

+75
-2
lines changed

4 files changed

+75
-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: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,32 @@ 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 calls this WHILE still inside the context created by __call__,
230+
so request tags have already been extracted and set. This method just
231+
needs to capture the exception directly.
232+
233+
Django converts view exceptions into responses before they propagate through
234+
the middleware stack, so the context manager in __call__/__acall__ never sees them.
235+
236+
Note: Django's process_exception is always synchronous, even for async views.
237+
"""
238+
if self.request_filter and not self.request_filter(request):
239+
return
240+
241+
if not self.capture_exceptions:
242+
return
243+
244+
# Context and tags already set by __call__ or __acall__
245+
# Just capture the exception
246+
if self.client:
247+
self.client.capture_exception(exception)
248+
else:
249+
from posthog import capture_exception
250+
251+
capture_exception(exception)

posthog/test/integrations/test_middleware.py

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

279+
def test_process_exception_integration(self):
280+
"""
281+
Integration test simulating Django's actual exception handling flow.
282+
283+
When a view raises an exception:
284+
1. Middleware.__call__ creates context with request tags
285+
2. __call__ calls self.get_response(request)
286+
3. Inside Django's handler: catches exception, checks if middleware.process_exception
287+
exists (hasattr), calls it if present, returns error response
288+
4. Context manager exits
289+
290+
This verifies exception capture works in the real Django flow.
291+
"""
292+
mock_client = Mock()
293+
294+
get_response = Mock(return_value=Mock())
295+
middleware = PosthogContextMiddleware(get_response)
296+
middleware.client = mock_client
297+
298+
view_exception = ValueError("View error")
299+
error_response = Mock(status_code=500)
300+
301+
def mock_get_response(request):
302+
# Simulate Django: check if process_exception exists, call it
303+
if hasattr(middleware, "process_exception"):
304+
middleware.process_exception(request, view_exception)
305+
return error_response
306+
307+
middleware.get_response = mock_get_response
308+
309+
request = MockRequest(
310+
headers={"X-POSTHOG-DISTINCT-ID": "user123"},
311+
method="POST",
312+
path="/api/test",
313+
)
314+
response = middleware(request)
315+
316+
self.assertEqual(response, error_response)
317+
mock_client.capture_exception.assert_called_once_with(view_exception)
318+
279319

280320
class TestPosthogContextMiddlewareAsync(unittest.TestCase):
281321
"""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)