Clycyo
Frameworks5 min read

Django Analytics: Templates, HTMX, and Server Events

The Django setup for cookieless web analytics: base template installation, HTMX partial navigation, and server-side conversion events.

Django's classic strength — server-rendered pages with real URLs — is the easy case for analytics: every navigation is a genuine page load. The interesting parts are the modern additions (HTMX partials) and Django's natural fit for server-side conversion events. Both, below.

Base template installation

{# templates/base.html #}
<head>
  <script
    defer
    src="https://clycyo.com/tracker.js"
    data-tracking-id="{{ CLYCYO_TRACKING_ID }}"
  ></script>
</head>

Expose the ID via a context processor reading settings — one place to change per environment. Full page loads arrive with complete navigation timing; nothing else to configure.

HTMX: partials vs navigations

HTMX swaps fragments without changing the URL — those are interactions, not pageviews, and are correctly not counted. When you use hx-push-url="true", the URL changes via the History API and the tracker records a pageview automatically. The rule of thumb falls out naturally: if it deserves a URL, it gets counted; if not, track it as an event where it matters:

<button hx-post="/subscribe" hx-swap="outerHTML"
        onclick="window.webanalytics?.track('newsletter_subscribed')">
  Subscribe
</button>

Identity

{% if user.is_authenticated %}
<script>
  window.webanalytics?.identify("{{ user.email|escapejs }}");
</script>
{% endif %}

Persist getVisitorId() onto the user model with a tiny authenticated endpoint — the join key for everything that follows.

Server-side conversions, the Django way

# e.g. in a Stripe webhook view or a post_save signal
import requests
from django.conf import settings

requests.post("https://clycyo.com/api/collect", json={
    "tracking_id": settings.CLYCYO_TRACKING_ID,
    "type": "event",
    "visitor_id": user.clycyo_visitor_id,
    "event_name": "subscription_paid",
    "event_properties": {
        "revenue": invoice.amount_paid / 100,
        "currency": invoice.currency.upper(),
        "order_id": invoice.id,
    },
})

Run it in a Celery task for retries; order_id keeps duplicates harmless. Signals make Django unusually good at this pattern — any model event (activation, upgrade, cancellation) can emit an analytics event without touching view code.

The takeaway

  • One template tag covers the whole site, classic or HTMX-flavored.
  • Cookieless means no consent banner on Django's GDPR checklist.
  • Server-side events + webhook revenue complete the attribution loop with tools Django developers already hold.