## 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
53 lines
1.8 KiB
Python
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'}"
|