plain.pageviews
Track pageviews from both client-side and server-side.
- Overview
- Client-side tracking
- Server-side tracking
- Attribution tracking
- Admin integration
- Data retention
- Settings
- FAQs
- Installation
Overview
You can track pageviews in two ways: client-side using JavaScript, or server-side directly from your views. Both methods store data in the same Pageview model and automatically extract attribution parameters from URLs.
from plain.pageviews.models import Pageview
# Server-side tracking example
def my_view(request):
Pageview.create_from_request(request, title="Product Page")
return TemplateResponse(request, "product.html")
For most use cases, client-side tracking is the simplest approach. Add the router to your URLs and include the JavaScript tag in your base template.
Client-side tracking
Client-side tracking uses a small JavaScript snippet that sends pageview data via the Beacon API. This captures the full browser URL, page title, referrer, and timestamp.
Add {% pageviews_js %} to your base template:
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
{% block content %}{% endblock %}
{% pageviews_js %}
</body>
</html>
The JavaScript runs asynchronously and sends a POST request to the tracking endpoint with:
url: The full browser URL (including query parameters)title: The document titlereferrer: The referring page URLtimestamp: Client-side timestamp in ISO 8601 format
Server-side tracking
You can track pageviews directly from your views using Pageview.create_from_request():
from plain.pageviews.models import Pageview
def checkout_view(request):
Pageview.create_from_request(request, title="Checkout")
# Your view logic here
return TemplateResponse(request, "checkout.html")
Server-side tracking differs from client-side tracking:
- The timestamp is generated on the server, not the client
- The referrer is extracted from the
Refererrequest header - The URL uses the request's full path via
request.build_absolute_uri() - Impersonation sessions are automatically ignored (no pageview is created)
All parameters are optional. You can override any value:
Pageview.create_from_request(
request,
url="https://example.com/custom-path",
title="Custom Title",
source="partner",
medium="referral",
campaign="summer_promo",
)
Attribution tracking
Pageviews automatically tracks traffic sources and campaigns from URL parameters. Three fields are captured:
- Source: Where the traffic came from (e.g., "google", "newsletter")
- Medium: How the traffic arrived (e.g., "cpc", "email", "social")
- Campaign: Which campaign generated the traffic (e.g., "summer_sale")
Supported parameters
UTM parameters (standard marketing tracking):
?utm_source=newsletter&utm_medium=email&utm_campaign=welcome_series
Simple ref parameter (developer-friendly alternative):
?ref=newsletter
Auto-detected tracking IDs (no configuration needed):
| Parameter | Source | Medium |
|---|---|---|
gclid |
cpc | |
fbclid |
social | |
msclkid |
bing | cpc |
ttclid |
tiktok | cpc |
twclid |
cpc |
Priority order
Parameters are processed in this order:
utm_sourcetakes priority overref- Auto-detected tracking IDs (gclid, fbclid, etc.) fill in values if UTM parameters are not present
- All values are normalized to lowercase
Attribution parameters are automatically extracted from the URL in both client-side and server-side tracking. The extraction happens server-side via extract_tracking_params().
Admin integration
The package includes a built-in admin viewset that shows all pageviews with filtering and search.
You can also add a pageviews card to your user admin detail view:
from plain.admin.views import AdminModelDetailView, AdminViewset, register_viewset
from plain.pageviews.admin import UserPageviewsCard
@register_viewset
class UserAdmin(AdminViewset):
class DetailView(AdminModelDetailView):
model = User
cards = [UserPageviewsCard]
The UserPageviewsCard displays the 50 most recent pageviews for that user.
For dashboard-level analytics, you can use PageviewsTrendCard which shows pageview counts over time.
Data retention
The package includes a chore that automatically cleans up old pageviews. The ClearOldPageviews chore runs according to your chores schedule and deletes:
- Anonymous pageviews older than 90 days (configurable)
- Authenticated pageviews older than 365 days (configurable)
Settings
| Setting | Default | Env var |
|---|---|---|
PAGEVIEWS_ASSOCIATE_ANONYMOUS_SESSIONS |
True |
PLAIN_PAGEVIEWS_ASSOCIATE_ANONYMOUS_SESSIONS |
PAGEVIEWS_ANONYMOUS_RETENTION_TIMEDELTA |
timedelta(days=90) |
- |
PAGEVIEWS_AUTHENTICATED_RETENTION_TIMEDELTA |
timedelta(days=365) |
- |
See default_settings.py for more details.
FAQs
When should I use server-side vs client-side tracking?
Client-side tracking (via {% pageviews_js %}) works best for:
- Standard web pages viewed by users
- Getting accurate client-side information (full URL with hash fragments, page title)
- Automatically tracking without adding code to every view
Server-side tracking (via Pageview.create_from_request()) works best for:
- Tracking specific user actions or events
- Guaranteed tracking that cannot be blocked by ad blockers or disabled JavaScript
- API endpoints or non-HTML responses
- Custom tracking logic based on business rules
Why not use server-side middleware for automatic tracking?
Tracking from the backend with middleware means you have to identify all kinds of requests not to track (assets, files, API calls, etc.). Client-side tracking naturally captures what you want in a more straightforward way, while server-side methods give you control when you need it.
How does anonymous session association work?
When PAGEVIEWS_ASSOCIATE_ANONYMOUS_SESSIONS is enabled (the default), pageviews from anonymous users are tracked with a session ID. When that user later logs in, all their previous anonymous pageviews are automatically associated with their user account. This gives you a complete picture of the user's journey before they registered or logged in.
What happens during impersonation?
When an admin is impersonating a user, pageviews are not tracked. This prevents admin activity from polluting the user's pageview history.
Installation
Install the package from PyPI:
uv add plain.pageviews
Add plain.pageviews to your INSTALLED_PACKAGES:
# app/settings.py
INSTALLED_PACKAGES = [
# ...
"plain.pageviews",
]
Run migrations to create the database table:
plain postgres sync
Add the router to your URLs:
# app/urls.py
from plain.urls import Router, include, path
from plain.pageviews.urls import PageviewsRouter
class AppRouter(Router):
namespace = ""
urls = [
# Your other URLs...
include("pageviews/", PageviewsRouter),
]
For client-side tracking, add the JavaScript tag to your base template:
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My App{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
{% pageviews_js %}
</body>
</html>
You can now track pageviews automatically via JavaScript, or manually from your views using Pageview.create_from_request(request).
Metadata
Release files for plain.pageviews 0.35.8
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| plain_pageviews-0.35.8.tar.gz | 17.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| plain_pageviews-0.35.8-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 43.2 kB
Release files / plain_pageviews-0.35.8.tar.gz
| Download URL | plain_pageviews-0.35.8.tar.gz |
|---|---|
| Size | 17.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c6fa3d3756a24317699d1ab043a4a1f66d706b37dae383268497ee975207e170
|
|
BLAKE2b-256 checksum How to use checksums |
45b791a6fbc75d47ffe2e592bd70721c0ff93b988eb0c124df764c3eb062f4c0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / plain_pageviews-0.35.8-py3-none-any.whl
| Download URL | plain_pageviews-0.35.8-py3-none-any.whl |
|---|---|
| Size | 25.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6d581bd5e3d4cd2517b8633e9edf8b67f1bed1454e2ef32e4718af30efe37a5e
|
|
BLAKE2b-256 checksum How to use checksums |
c41b6e9e87d8b308e5c14e7978b021e90d2e498472819bcf44bcb45638c65dc7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|