Privatus Analytics for Python
Real visitor numbers for your Python web app. No cookies, no cookie pop-up, no bots.
You want to know if people use your Python web app. But Google Analytics uses cookies, so in many countries you must show a cookie pop-up first (the box that asks visitors to say yes to cookies). Many people click no, so they never show up in your numbers. Bots add fake visits on top.
This package adds the Privatus Analytics script to your Django, Flask, FastAPI or Starlette app, with a template tag or a middleware that puts it in the page head for you. The script sets no cookies and keeps no IP addresses. Bots are filtered out before they are counted.
So you see how many real people came, which pages they looked at and where they came from, on one simple dashboard. A report comes to your inbox each week, and you can set alerts for when traffic jumps or drops. And you don't need a cookie pop-up for your analytics.
Free plan, no card: 10,000 events a month (an event is a page view or an action you track) on up to 3 sites. Start free at privatusanalytics.com.
- No cookies. No cookies and no hidden IDs on your visitors' devices, and no IP addresses are kept. Laws differ by country, so check your own legal advice.
- Real people, not bots. Known bots, data centers and spam visits are left out on every plan.
- Small and fast. The script is about 3 KB when compressed.
What this package does
- Works with Django, Flask, FastAPI and Starlette.
- A template tag, and an optional middleware that puts the script in the
<head>for you. - The package only prints HTML: the browser script tag and a few HTML attributes. It never sends events from your server, sets no cookies and reads nothing about your visitors.
You need a Privatus Analytics account to see your reports, and there is a free plan. Sign up at privatusanalytics.com.
Requirements
- Python 3.9 or later.
- Django 4.2 or later, Flask 2.3 or later, or Starlette 0.29 or later (FastAPI is built on Starlette). The core works with no framework at all.
- A Privatus Analytics site id (it starts with
pa_). You find it under Site settings, Tracking.
Install
pip install privatus-analytics
The package has no required dependencies. It uses the framework you already have. For the full setup guide and every tracker option, see the Privatus Analytics docs.
Set your site id with an environment variable:
PRIVATUS_SITE_ID=pa_YOURSITEID
or in your settings, as shown for each framework below.
Add the tag
Django
Add the app and your settings in settings.py:
INSTALLED_APPS = [
# ...
"privatus_analytics.django",
]
PRIVATUS_ANALYTICS = {
"site_id": "pa_YOURSITEID",
}
Put the tag in the <head> of your base template:
{% load privatus %}
<head>
<title>My app</title>
{% privatus_analytics %}
</head>
The tag prints only when DEBUG is off. It needs the request in the
template context (the default django.template.context_processors.request
adds it) for the nonce and for page props that depend on the request.
Flask
from flask import Flask
from privatus_analytics.flask import PrivatusAnalytics
app = Flask(__name__)
app.config["PRIVATUS_ANALYTICS"] = {"site_id": "pa_YOURSITEID"}
PrivatusAnalytics(app) # or PrivatusAnalytics().init_app(app)
<head>
<title>My app</title>
{{ privatus_analytics() }}
</head>
The tag prints only when the app is not in debug or testing mode.
FastAPI and Starlette
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from privatus_analytics import Config
from privatus_analytics.starlette import setup_templates
config = Config(site_id="pa_YOURSITEID")
templates = setup_templates(Jinja2Templates(directory="templates"), config)
app = FastAPI()
@app.get("/")
async def home(request: Request):
return templates.TemplateResponse(request, "home.html")
Use {{ privatus_analytics() }} in the template, the same as Flask.
Starlette has no shared "production" switch, so the tag prints only when the
PRIVATUS_ENV environment variable is production, or when you set
enabled=True yourself, for example Config(enabled=not settings.debug).
Without a framework
from privatus_analytics import Config, render_tag
html = render_tag(Config(site_id="pa_YOURSITEID", enabled=True))
To add the tag to any Jinja2 environment, call
privatus_analytics.jinja.install(env, config).
What it prints
In production it prints:
<script defer src="https://privatusstats.com/js/pa.js" data-site="pa_YOURSITEID"></script>
The tag prints nothing when there is no site id, and nothing in development (see below). The one exception is the queue stub: when you turn it on, it still prints. Every value is HTML-escaped.
If your app uses a Content Security Policy (CSP, a list of rules that tells
the browser which scripts may run) with a nonce (a random code that marks a
script as allowed), the tag adds it for you. It looks for request.csp_nonce
(django-csp, Flask-Talisman), a csp_nonce template value (Django 6) and
request.state.csp_nonce (Starlette and FastAPI). You can also set the
nonce setting or pass nonce= to the tag.
Or let the package add it
If you would rather not touch your templates, switch on auto inject:
PRIVATUS_ANALYTICS = {"site_id": "pa_YOURSITEID", "auto_inject": True}
-
Django: also add
"privatus_analytics.django.middleware.PrivatusAnalyticsMiddleware"toMIDDLEWARE. Put it belowGZipMiddlewarein the list. -
Flask: nothing else to do. The extension adds an
after_requesthook. -
FastAPI and Starlette: add the ASGI middleware:
from privatus_analytics.starlette import PrivatusAnalyticsMiddleware app.add_middleware(PrivatusAnalyticsMiddleware, config=Config(site_id="pa_YOURSITEID", auto_inject=True))
It adds the tag just before </head> in uncompressed text/html responses
with status 200, and fixes the Content-Length header. It skips Ajax
requests (background requests sent by JavaScript), pages without a <head>,
and pages that already have the tag. Responses that have a
Content-Encoding header (compressed responses) are left alone, so add the
middleware where it runs before compression, or use the template tag.
Configuration
Every setting is optional except the site id. Django reads them from
settings.PRIVATUS_ANALYTICS, Flask from app.config["PRIVATUS_ANALYTICS"],
and Starlette takes a Config(...) or a plain dict. Unknown setting names
raise an error, so typos do not go unnoticed.
PRIVATUS_ANALYTICS = {
"site_id": "pa_YOURSITEID", # or PRIVATUS_SITE_ID
"script_host": "https://privatusstats.com", # or PRIVATUS_SCRIPT_HOST
"modules": ["engage", "auto", "vitals", "clicks", "ask"],
"excluded_paths": ["/admin/*", "/preview/*"],
"domains": ["example.com", "www.example.com"],
"spa": "auto", # auto, history, hash or off
"manual": False, # True turns off the automatic pageview
"auto_inject": False,
"extra_attributes": {"data-mask": "/orders/*->/orders/:id"},
"props": lambda request: {"plan": "pro"},
}
| Setting | Default | What it does |
|---|---|---|
site_id |
PRIVATUS_SITE_ID |
The data-site attribute. Nothing prints without it |
script_host |
https://privatusstats.com |
Where /js/pa.js is served. Change it only if you host your own copy |
modules |
tracker default (engage) |
The data-modules attribute. Setting it replaces the default, so keep engage for time on page and scroll depth. [] loads none |
excluded_paths |
none | The data-exclude attribute. Path patterns the tracker never counts |
domains |
all hosts | The data-domains attribute. Count only these hostnames |
spa |
auto |
The data-spa attribute, for single-page apps |
manual |
False |
The data-manual attribute |
enabled |
None |
True or False forces the tag on or off. None uses the framework (see below) |
enabled_environments |
("production",) |
Used when no framework decides. Compared with environment |
environment |
PRIVATUS_ENV, else development |
The current environment when no framework decides |
auto_inject |
False |
Adds the tag with a middleware |
extra_attributes |
{} |
Any other data-* attribute. True prints the name alone |
nonce |
framework nonce | The CSP nonce, if your framework does not give one |
props |
None |
Page props: a dict, or a function that gets the request and returns a dict |
queue_stub |
False |
Prints the window.privatus queue stub before the tag |
Each tracker attribute is explained in the script attributes reference, and the modules in Modules.
You can also change a setting for one page:
{% privatus_analytics modules="engage,auto" excluded_paths="/preview/*" %}
{{ privatus_analytics(modules=["engage", "auto"], excluded_paths=["/preview/*"]) }}
Development and test
Your own clicking around should not reach your reports, so by default the tag prints nothing:
- Django: when
DEBUGis on. - Flask: when
app.debugorapp.testingis on. - Starlette, FastAPI and plain Python: unless
PRIVATUS_ENV=production.
If queue_stub is on, the tag still prints the small queue stub (and nothing
else), so privatus.track calls in your own JavaScript keep working. They
wait in a queue and are never sent.
To see the tag while you build, set "enabled": True. The tracker also
ignores localhost unless you pass data-allow-local or data-debug (see
Debug mode):
PRIVATUS_ANALYTICS = {"site_id": "pa_YOURSITEID", "enabled": True, "extra_attributes": {"data-debug": "true"}}
Leaving people out
Use excluded_paths (the data-exclude attribute) for admin areas. To leave
out signed-in staff, print the tag only for everyone else:
{% if not request.user.is_staff %}{% privatus_analytics %}{% endif %}
Product analytics: see which features people use
Buttons, links and forms
privatus_event prints the attributes that make the tracker send a custom
event when someone clicks the element:
<button {% privatus_event "Report exported" format="csv" source="toolbar" %}>Export</button>
<button {{ privatus_event("Report exported", format="csv", source="toolbar") }}>Export</button>
Both print:
<button data-privatus-event="Report exported" data-privatus-prop-format="csv" data-privatus-prop-source="toolbar">Export</button>
Put it on a <form> and the event is sent when the form is submitted:
<form method="post" {% privatus_event "Newsletter signup" list="weekly" %}>
The rules:
- The event name is trimmed and cut to 120 characters. An empty name prints nothing.
- Detail names (props) are made lowercase, any character other than
a-z,0-9and_becomes_, and they are cut to 64 characters. At most 30 props are kept. - Values become text:
Truebecomestrue, numbers stay as written, and lists are joined with commas.Nonevalues are dropped. Values are cut to 500 characters. - These attributes print even when the tag is turned off. They do nothing without the tracker, and your development and production HTML stay the same.
In plain Python, use event_attrs("Report exported", {"format": "csv"}).
Click maps
Give important elements a stable name so click maps can group clicks on them:
<a {% privatus_click "pricing.cta" %} href="/pricing">See pricing</a>
<a {{ privatus_click("pricing.cta") }} href="/pricing">See pricing</a>
This prints data-privatus-click="pricing.cta". Names may use letters,
digits and _ . : -, up to 100 characters. Anything else prints nothing and
logs a warning on the privatus_analytics logger. In plain Python, use
click_attr("pricing.cta"). In Jinja2, privatus_event and privatus_click
also work as filters: {{ "pricing.cta" | privatus_click }}.
Page props: plan, role and other groups
Page props are sent with every pageview and event on the page. Use them to compare groups, such as plan, role, app version, feature flags or account size. Set them in your settings as a function that gets the request:
def privatus_props(request):
if not request.user.is_authenticated:
return {"plan": "visitor"}
return {"plan": request.user.account.plan, "role": request.user.role, "app_version": "4.2"}
PRIVATUS_ANALYTICS = {"site_id": "pa_YOURSITEID", "props": privatus_props}
or pass them to one tag, which adds to the props from your settings:
{% privatus_analytics props=page_props %}
The tag then prints a small script before the tracker, and points the tracker
at it with data-props:
<script>window.privatusPageProps=function(){return {"plan":"pro","role":"admin"}}</script>
<script defer src="https://privatusstats.com/js/pa.js" data-site="pa_YOURSITEID" data-props="privatusPageProps"></script>
Detail names follow the same rules as events. Numbers and True/False
stay numbers and booleans, and lists stay lists (at most 30 items). The data
is escaped so it is safe inside a <script> element.
Never put names, emails, user ids or anything else that points to one person in props. Privatus Analytics counts visitors without any id. Put groups (plan, role), not people. The server also removes emails and long ids it finds.
Track from your own JavaScript
The package only loads the tracker. You can also send events from your own scripts, as described in the JavaScript API docs:
privatus.track("Feature used", { feature: "export" });
If your code may run before the tracker has loaded, set "queue_stub": True.
The tag then prints this small script first (with your nonce), which keeps
the calls until the tracker is ready:
<script>window.privatus=window.privatus||{q:[]};['track','pageview','props','optOut','optIn','ready'].forEach(function(m){window.privatus[m]=window.privatus[m]||function(){window.privatus.q.push([m,Array.prototype.slice.call(arguments)])}})</script>
The stub prints even when the tag is off (in development, or with
"enabled": False). Only the stub prints then: no tracker and no page
props. So your privatus.track calls never fail. They wait in the queue and
are never sent.
In plain Python, queue_stub(nonce=...) returns the same script.
Naming events
Name events as "Noun verb", in the past tense, like Report exported,
Invite sent or Plan upgraded. Keep the name short and put details in
props (format: csv), so one event covers every variant. Then use them in:
- Funnels, to see where people drop off.
- Click maps, to see what people click.
- Goals, to count conversions.
- Events, for every event and its props.
Privacy
The script sets no cookies and stores nothing in the browser. This package sends nothing from your server and reads no IP addresses, browser details or visitor ids. See the privacy documentation for what is collected.
If you have a privacy policy, it should say which tools handle your visitors' data. You can paste this:
This website uses Privatus Analytics to count visits. It does not use cookies and does not follow you from one website to another. Your IP address is used for a moment to count visits and is never stored.
Change it to fit your site. The privacy policy generator makes a longer version with your site's name in it.
Development
The tests run in Docker, so your local Python does not matter:
./run-docker.sh
It runs the pytest suite against small Django, Flask and Starlette apps, Ruff (the linter and formatter), and the package build.
Contributing
Bug reports and pull requests are welcome at https://github.com/Privatus-Analytics/python.
License
MIT. See LICENSE. Privatus Analytics is a product of Two Phase LLC, Wyoming, USA.
Metadata
Release files for privatus-analytics 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| privatus_analytics-1.0.1.tar.gz | 23.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| privatus_analytics-1.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 44.9 kB
Release files / privatus_analytics-1.0.1.tar.gz
| Download URL | privatus_analytics-1.0.1.tar.gz |
|---|---|
| Size | 23.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
854f32755529c88346e370967f11775ff6fabc0251a53a9f64d851d8bea893ee
|
|
BLAKE2b-256 checksum How to use checksums |
e4cb6fb35b7769ab6a945bedefdc11f35f333a4a2a15ccd48cb123f00ef1af13
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logRelease files / privatus_analytics-1.0.1-py3-none-any.whl
| Download URL | privatus_analytics-1.0.1-py3-none-any.whl |
|---|---|
| Size | 21.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cd753067774f19a269c14237b8cf8b5abde4a4da672d7fc73dd0edfc8c2da3ec
|
|
BLAKE2b-256 checksum How to use checksums |
a75efff4221dbf68e61d410015c7637c5bc5eb80a4510717f1cd8c8602ccaf69
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logSecurity for privatus-analytics 1.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for privatus_analytics-1.0.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.