Skip to content

Commit 97c9048

Browse files
authored
Merge branch 'master' into feat/add-web-search-count-in-usage
2 parents e6e417c + 3e52e7f commit 97c9048

25 files changed

+9045
-44
lines changed

.github/workflows/ci.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ name: CI
33
on:
44
- pull_request
55

6+
permissions:
7+
contents: read
8+
69
jobs:
710
code-quality:
811
name: Code quality checks
@@ -68,3 +71,34 @@ jobs:
6871
- name: Run posthog tests
6972
run: |
7073
pytest --verbose --timeout=30
74+
75+
django5-integration:
76+
name: Django 5 integration tests
77+
runs-on: ubuntu-latest
78+
79+
steps:
80+
- uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2
81+
with:
82+
fetch-depth: 1
83+
84+
- name: Set up Python 3.12
85+
uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55
86+
with:
87+
python-version: 3.12
88+
89+
- name: Install uv
90+
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
91+
with:
92+
enable-cache: true
93+
pyproject-file: 'integration_tests/django5/pyproject.toml'
94+
95+
- name: Install Django 5 test project dependencies
96+
shell: bash
97+
working-directory: integration_tests/django5
98+
run: |
99+
UV_PROJECT_ENVIRONMENT=$pythonLocation uv sync
100+
101+
- name: Run Django 5 middleware integration tests
102+
working-directory: integration_tests/django5
103+
run: |
104+
uv run pytest test_middleware.py test_exception_capture.py --verbose

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ pyrightconfig.json
1919
.env
2020
.DS_Store
2121
posthog-python-references.json
22+
.claude/settings.local.json

CHANGELOG.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
1-
# 6.8.0 - 2025-10-28
1+
# 6.8.0 - 2025-11-03
22

33
- feat(llma): send web search calls to be used for LLM cost calculations
44

5+
# 6.7.14 - 2025-11-03
6+
7+
- fix(django): Handle request.user access in async middleware context to prevent SynchronousOnlyOperation errors in Django 5+ (fixes #355)
8+
- test(django): Add Django 5 integration test suite with real ASGI application testing async middleware behavior
9+
10+
# 6.7.13 - 2025-11-02
11+
12+
- fix(llma): cache cost calculation in the LangChain callback
13+
14+
# 6.7.12 - 2025-11-02
15+
16+
- fix(django): Restore process_exception method to capture view and downstream middleware exceptions (fixes #329)
17+
- fix(ai/langchain): Add LangChain 1.0+ compatibility for CallbackHandler imports (fixes #362)
18+
519
# 6.7.11 - 2025-10-28
620

721
- feat(ai): Add `$ai_framework` property for framework integrations (e.g. LangChain)
822

923
# 6.7.10 - 2025-10-24
1024

1125
- fix(django): Make middleware truly hybrid - compatible with both sync (WSGI) and async (ASGI) Django stacks without breaking sync-only deployments
12-
- fix(django): Exception capture works correctly via context manager (addresses #329)
1326

1427
# 6.7.9 - 2025-10-22
1528

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
db.sqlite3
2+
*.pyc
3+
__pycache__/
4+
.pytest_cache/
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env python
2+
"""Django's command-line utility for administrative tasks."""
3+
4+
import os
5+
import sys
6+
7+
8+
def main():
9+
"""Run administrative tasks."""
10+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testdjango.settings")
11+
try:
12+
from django.core.management import execute_from_command_line
13+
except ImportError as exc:
14+
raise ImportError(
15+
"Couldn't import Django. Are you sure it's installed and "
16+
"available on your PYTHONPATH environment variable? Did you "
17+
"forget to activate a virtual environment?"
18+
) from exc
19+
execute_from_command_line(sys.argv)
20+
21+
22+
if __name__ == "__main__":
23+
main()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[project]
2+
name = "test-django5"
3+
version = "0.1.0"
4+
requires-python = ">=3.12"
5+
dependencies = [
6+
"django~=5.2.7",
7+
"uvicorn[standard]~=0.38.0",
8+
"posthog",
9+
"pytest~=8.4.2",
10+
"pytest-asyncio~=1.2.0",
11+
"pytest-django~=4.11.1",
12+
"httpx~=0.28.1",
13+
]
14+
15+
[tool.uv]
16+
required-version = ">=0.5"
17+
18+
[tool.uv.sources]
19+
posthog = { path = "../..", editable = true }
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""
2+
Test that verifies exception capture functionality.
3+
4+
These tests verify that exceptions are actually captured to PostHog, not just that
5+
500 responses are returned.
6+
7+
Without process_exception(), view exceptions are NOT captured to PostHog (v6.7.11 and earlier).
8+
With process_exception(), Django calls this method to capture exceptions before
9+
converting them to 500 responses.
10+
"""
11+
12+
import os
13+
import django
14+
15+
# Setup Django before importing anything else
16+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testdjango.settings")
17+
django.setup()
18+
19+
import pytest # noqa: E402
20+
from httpx import AsyncClient, ASGITransport # noqa: E402
21+
from django.core.asgi import get_asgi_application # noqa: E402
22+
23+
24+
@pytest.fixture(scope="session")
25+
def asgi_app():
26+
"""Shared ASGI application for all tests."""
27+
return get_asgi_application()
28+
29+
30+
@pytest.mark.asyncio
31+
async def test_async_exception_is_captured(asgi_app):
32+
"""
33+
Test that async view exceptions are captured to PostHog.
34+
35+
The middleware's process_exception() method ensures exceptions are captured.
36+
Without it (v6.7.11 and earlier), exceptions are NOT captured even though 500 is returned.
37+
"""
38+
from unittest.mock import patch
39+
40+
# Track captured exceptions
41+
captured = []
42+
43+
def mock_capture(exception, **kwargs):
44+
"""Mock capture_exception to record calls."""
45+
captured.append(
46+
{
47+
"exception": exception,
48+
"type": type(exception).__name__,
49+
"message": str(exception),
50+
}
51+
)
52+
53+
# Patch at the posthog module level where middleware imports from
54+
with patch("posthog.capture_exception", side_effect=mock_capture):
55+
async with AsyncClient(
56+
transport=ASGITransport(app=asgi_app), base_url="http://testserver"
57+
) as ac:
58+
response = await ac.get("/test/async-exception")
59+
60+
# Django returns 500
61+
assert response.status_code == 500
62+
63+
# CRITICAL: Verify PostHog captured the exception
64+
assert len(captured) > 0, "Exception was NOT captured to PostHog!"
65+
66+
# Verify it's the right exception
67+
exception_data = captured[0]
68+
assert exception_data["type"] == "ValueError"
69+
assert "Test exception from Django 5 async view" in exception_data["message"]
70+
71+
72+
@pytest.mark.asyncio
73+
async def test_sync_exception_is_captured(asgi_app):
74+
"""
75+
Test that sync view exceptions are captured to PostHog.
76+
77+
The middleware's process_exception() method ensures exceptions are captured.
78+
Without it (v6.7.11 and earlier), exceptions are NOT captured even though 500 is returned.
79+
"""
80+
from unittest.mock import patch
81+
82+
# Track captured exceptions
83+
captured = []
84+
85+
def mock_capture(exception, **kwargs):
86+
"""Mock capture_exception to record calls."""
87+
captured.append(
88+
{
89+
"exception": exception,
90+
"type": type(exception).__name__,
91+
"message": str(exception),
92+
}
93+
)
94+
95+
# Patch at the posthog module level where middleware imports from
96+
with patch("posthog.capture_exception", side_effect=mock_capture):
97+
async with AsyncClient(
98+
transport=ASGITransport(app=asgi_app), base_url="http://testserver"
99+
) as ac:
100+
response = await ac.get("/test/sync-exception")
101+
102+
# Django returns 500
103+
assert response.status_code == 500
104+
105+
# CRITICAL: Verify PostHog captured the exception
106+
assert len(captured) > 0, "Exception was NOT captured to PostHog!"
107+
108+
# Verify it's the right exception
109+
exception_data = captured[0]
110+
assert exception_data["type"] == "ValueError"
111+
assert "Test exception from Django 5 sync view" in exception_data["message"]

0 commit comments

Comments
 (0)