Add public PageView tracking for portal analytics (#4)
## Summary Closes #3. - Add `PageView` model + migration for successful public marketing GET hits - Wire `PublicPageViewMiddleware` (skips portal/admin/accounts/api/static/health/etc.; fails soft on DB errors) - Portal analytics report: last-30-day views + top pages table - Admin registration and tests ## Test plan - [ ] Apply migration `analytics.0002_pageview` - [ ] Hit `/` and `/about/` → rows in `PageView` / admin - [ ] Hit `/healthz/`, `/portal/`, `/admin/` → no new pageviews - [ ] Open portal Analytics → views count + Top pages look right - [ ] `uv run python site/manage.py test analytics.tests` Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
@@ -1,10 +1,54 @@
|
||||
import logging
|
||||
|
||||
from django.utils.crypto import get_random_string
|
||||
|
||||
from analytics.models import Attribution, UTMVisit
|
||||
from analytics.models import Attribution, PageView, UTMVisit
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CORRELATION_COOKIE = "ms_cid"
|
||||
|
||||
# Portal, auth, health, assets, and transactional public endpoints.
|
||||
_SKIP_PREFIXES = (
|
||||
"/portal/",
|
||||
"/admin/",
|
||||
"/accounts/",
|
||||
"/api/",
|
||||
"/healthz",
|
||||
"/static/",
|
||||
"/media/",
|
||||
"/unsubscribe/",
|
||||
)
|
||||
_SKIP_PATHS = frozenset({"/robots.txt", "/sitemap.xml", "/favicon.ico"})
|
||||
|
||||
|
||||
class PublicPageViewMiddleware:
|
||||
"""Record successful GET hits on public marketing pages."""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
if _should_record_page_view(request, response):
|
||||
try:
|
||||
PageView.objects.create(path=request.path[:512])
|
||||
except Exception:
|
||||
logger.exception("Failed to record page view for %s", request.path)
|
||||
return response
|
||||
|
||||
|
||||
def _should_record_page_view(request, response) -> bool:
|
||||
if request.method != "GET":
|
||||
return False
|
||||
if getattr(response, "status_code", 0) != 200:
|
||||
return False
|
||||
path = request.path
|
||||
if path in _SKIP_PATHS:
|
||||
return False
|
||||
return not any(path.startswith(prefix) for prefix in _SKIP_PREFIXES)
|
||||
|
||||
|
||||
class UTMTrackingMiddleware:
|
||||
"""Capture UTM params into UTMVisit and stash a correlation id cookie."""
|
||||
|
||||
Reference in New Issue
Block a user