|
| 1 | +import requests |
| 2 | +import structlog |
| 3 | +from django.conf import settings |
| 4 | + |
| 5 | +from news.models import Entry |
| 6 | +from reports.constants import WEB_ANALYTICS_API_URL_V2, WEB_ANALYTICS_DOMAIN |
| 7 | + |
| 8 | +logger = structlog.get_logger(__name__) |
| 9 | + |
| 10 | +NEWS_ENTRY_PREFIX = "/news/entry/" |
| 11 | + |
| 12 | + |
| 13 | +def fetch_post_views() -> dict[str, int]: |
| 14 | + """Return a slug-to-pageviews mapping fetched from Plausible API v2. |
| 15 | +
|
| 16 | + Returns an empty dict if the API key is not configured or the response |
| 17 | + contains no matching URLs. |
| 18 | + Raises requests.HTTPError on a non-2xx response. |
| 19 | + """ |
| 20 | + if not settings.PLAUSIBLE_STATS_KEY or settings.PLAUSIBLE_STATS_KEY == "changeme": |
| 21 | + logger.info( |
| 22 | + "fetch_post_views.skipped", reason="PLAUSIBLE_STATS_KEY not configured" |
| 23 | + ) |
| 24 | + return {} |
| 25 | + |
| 26 | + payload = { |
| 27 | + "site_id": WEB_ANALYTICS_DOMAIN, |
| 28 | + "metrics": ["pageviews"], |
| 29 | + "dimensions": ["event:page"], |
| 30 | + "filters": [["contains", "event:page", [NEWS_ENTRY_PREFIX]]], |
| 31 | + "date_range": "all", |
| 32 | + } |
| 33 | + headers = { |
| 34 | + "Content-Type": "application/json; charset=utf-8", |
| 35 | + "Authorization": f"Bearer {settings.PLAUSIBLE_STATS_KEY}", |
| 36 | + } |
| 37 | + |
| 38 | + response = requests.post( |
| 39 | + url=WEB_ANALYTICS_API_URL_V2, json=payload, headers=headers |
| 40 | + ) |
| 41 | + if not response.ok: |
| 42 | + raise requests.HTTPError( |
| 43 | + f"Plausible API error {response.status_code}: {response.text}", |
| 44 | + response=response, |
| 45 | + ) |
| 46 | + |
| 47 | + data = response.json() |
| 48 | + if not data or "results" not in data: |
| 49 | + raise ValueError(f"Unexpected Plausible API response: {data}") |
| 50 | + |
| 51 | + slug_views: dict[str, int] = {} |
| 52 | + for result in data["results"]: |
| 53 | + path = result["dimensions"][0] |
| 54 | + if not path.startswith(NEWS_ENTRY_PREFIX): |
| 55 | + continue |
| 56 | + slug = path[len(NEWS_ENTRY_PREFIX) :].rstrip("/") |
| 57 | + if slug: |
| 58 | + slug_views[slug] = int(result["metrics"][0]) |
| 59 | + |
| 60 | + return slug_views |
| 61 | + |
| 62 | + |
| 63 | +def update_page_views(slug_views: dict[str, int], entries: list | None = None) -> int: |
| 64 | + """Bulk-update Entry.page_views from a slug-to-count mapping. |
| 65 | +
|
| 66 | + Returns the number of entries updated. |
| 67 | + """ |
| 68 | + if not slug_views: |
| 69 | + return 0 |
| 70 | + |
| 71 | + if entries is None: |
| 72 | + entries = list(Entry.objects.filter(slug__in=slug_views.keys())) |
| 73 | + unmatched = set(slug_views) - {e.slug for e in entries} |
| 74 | + if unmatched: |
| 75 | + logger.warning("update_page_views.unmatched_slugs", slugs=sorted(unmatched)) |
| 76 | + |
| 77 | + for entry in entries: |
| 78 | + entry.page_views = slug_views[entry.slug] |
| 79 | + |
| 80 | + Entry.objects.bulk_update(entries, ["page_views"]) |
| 81 | + return len(entries) |
0 commit comments