Track pageviews from the client-side.
Project description
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).
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file plain_pageviews-0.35.1.tar.gz.
File metadata
- Download URL: plain_pageviews-0.35.1.tar.gz
- Upload date:
- Size: 17.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f03545535f86b15b684a078f77ca887ed44631c425ca3cd543e6ffe3dd4c354d
|
|
| MD5 |
93121c8eb85fcc41999624aef6647d16
|
|
| BLAKE2b-256 |
35461bbde93f2263947091b448b467b0977359077d12dab6519e3f26e710cd30
|
File details
Details for the file plain_pageviews-0.35.1-py3-none-any.whl.
File metadata
- Download URL: plain_pageviews-0.35.1-py3-none-any.whl
- Upload date:
- Size: 24.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
424a528f26eb4189cbb1ee20fac030bdf153c3e4c6c0c560607b62335cb52e80
|
|
| MD5 |
e0acbd42fac19d0984f4768caa9750fd
|
|
| BLAKE2b-256 |
34d5babc4e9f8170a7ac131f8ecf2fdbd9987c4dc5f5fe7171946d96f59a0e37
|