Files
monica_site/site/analytics/models.py
T
westfarn 46abaa6917
Deploy Beta / unit-tests (push) Successful in 12s
Deploy Beta / docker (push) Successful in 22s
Deploy Beta / deploy-beta (push) Successful in 5m40s
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
2026-08-20 03:34:56 -07:00

53 lines
1.8 KiB
Python

from django.db import models
from core.models import TimeStampedModel, UUIDPrimaryKeyModel
from leads.models import Lead
class PageView(UUIDPrimaryKeyModel, TimeStampedModel):
"""One successful GET of a public marketing page."""
path = models.CharField(max_length=512, db_index=True)
class Meta:
ordering = ["-created_at"]
indexes = [
models.Index(fields=["created_at", "path"]),
]
def __str__(self) -> str:
return self.path or "/"
class UTMVisit(UUIDPrimaryKeyModel, TimeStampedModel):
correlation_id = models.CharField(max_length=64, db_index=True)
path = models.CharField(max_length=512, blank=True)
referrer = models.URLField(blank=True, max_length=1024)
utm_source = models.CharField(max_length=128, blank=True)
utm_medium = models.CharField(max_length=128, blank=True)
utm_campaign = models.CharField(max_length=128, blank=True)
utm_term = models.CharField(max_length=128, blank=True)
utm_content = models.CharField(max_length=128, blank=True)
user_agent = models.CharField(max_length=512, blank=True)
class Meta:
ordering = ["-created_at"]
def __str__(self) -> str:
return f"{self.utm_source or 'direct'} / {self.path}"
class Attribution(TimeStampedModel):
lead = models.OneToOneField(
Lead, on_delete=models.CASCADE, related_name="attribution"
)
visit = models.ForeignKey(
UTMVisit, null=True, blank=True, on_delete=models.SET_NULL
)
utm_source = models.CharField(max_length=128, blank=True)
utm_medium = models.CharField(max_length=128, blank=True)
utm_campaign = models.CharField(max_length=128, blank=True)
def __str__(self) -> str:
return f"attr {self.lead_id}{self.utm_source or 'direct'}"