Plain is headed towards 1.0! Subscribe for development updates →

plain-pageviews

Track pageviews from the client-side.

Installation

Install plain.pageviews and add it to INSTALLED_PACKAGES.

Add PageviewsRouter to your urls.

Add {% pageviews_js %} to your base.html template to include the tracking code on the client side.

Admin integration

from plain.pageviews.admin import UserPageviewsCard


@register_viewset
class UserAdmin(AdminViewset):
    class DetailView(AdminModelDetailView):
        model = User
        cards = [UserPageviewsCard]

FAQs

Why not use server-side middleware?

Originally this was the idea. It turns out that tracking from the backend, while powerful, also means you have to identify all kinds of requests not to track (assets, files, API calls, etc.). In the end, a simple client-side tracking script naturally accomplishes what we're looking for in a more straightforward way.

 1import json
 2
 3from plain.runtime import settings
 4from plain.views import View
 5from plain.views.csrf import CsrfExemptViewMixin
 6
 7from .models import Pageview
 8
 9
10class TrackView(CsrfExemptViewMixin, View):
11    def post(self):
12        if getattr(self.request, "impersonator", None):
13            # Don't track page views if we're impersonating a user
14            return 200
15
16        data = json.loads(self.request.body)
17
18        url = data["url"]
19        title = data["title"]
20        referrer = data["referrer"]
21        timestamp = data["timestamp"]
22
23        if user := getattr(self.request, "user", None):
24            user_id = user.pk
25        else:
26            user_id = ""
27
28        if session := getattr(self.request, "session", None):
29            session_key = session.session_key or ""
30
31            if settings.PAGEVIEWS_ASSOCIATE_ANONYMOUS_SESSIONS:
32                if not user_id:
33                    if not session_key:
34                        # Make sure we have a key to use
35                        session.create()
36                        session_key = session.session_key
37
38                    # The user hasn't logged in yet but might later. When they do log in,
39                    # the session key itself will be cycled (session fixation attacks),
40                    # so we'll store the anonymous session id in the data which will be preserved
41                    # when the key cycles, then remove it immediately after.
42                    session["pageviews_anonymous_session_key"] = session_key
43                elif user_id and "pageviews_anonymous_session_key" in session:
44                    # Associate the previously anonymous pageviews with the user
45                    Pageview.objects.filter(
46                        user_id="",
47                        session_key=session["pageviews_anonymous_session_key"],
48                    ).update(user_id=user_id)
49
50                    # Remove it so we don't keep trying to associate it
51                    del session["pageviews_anonymous_session_key"]
52        else:
53            session_key = ""
54
55        Pageview.objects.create(
56            user_id=user_id,
57            session_key=session_key,
58            url=url,
59            title=title,
60            referrer=referrer,
61            timestamp=timestamp,
62        )
63
64        return 201