Install guide
Add website analytics to Django
templates/base.html, behind a settings flag you define yourself.
The tag goes in your base template’s <head> - templates/base.html in most projects - behind a flag that’s off everywhere except production.
<script defer src="https://app.feasible.lol/js/fs-k7m2q4x5r3n6t2v5.js"></script>Steps
- Add a setting. In
settings.py:
FEASIBLE_ENABLED = not DEBUG and os.environ.get("DJANGO_ENV") == "production"
- Expose it with a two-line context processor, in
yourapp/context_processors.py:
from django.conf import settings
def analytics(request):
return {"feasible_enabled": settings.FEASIBLE_ENABLED}
Register it in
TEMPLATES→OPTIONS→context_processors, alongside the ones already there.Put the tag in
templates/base.html, inside<head>:
{% if feasible_enabled %}
<script defer
src="https://app.feasible.lol/js/fs-k7m2q4x5r3n6t2v5.js"></script>
{% endif %}
Every template that does {% extends "base.html" %} now carries it.
Django’s debug variable isn’t the guard you want
Because debug isn’t what it looks like.
Django’s django.template.context_processors.debug only sets that variable when two things hold: DEBUG is True and the request’s REMOTE_ADDR is in INTERNAL_IPS. Off your dev machine, or with INTERNAL_IPS unset - which is the default - debug is absent from the context entirely.
An absent variable is falsey. So {% if not debug %} evaluates true on your laptop, and the guard you wrote to keep development out of your numbers does nothing at all. It doesn’t fail loudly; it just quietly tracks everything.
Your own flag is two lines and it means what it says.
Staging is the real risk
localhost and 127.0.0.1 are never counted, with or without a guard, so runserver was never the problem.
The problem is a staging deployment on a real hostname with production settings copied across. That traffic lands in your live numbers and can’t be unpicked afterwards. Set the environment variable properly, or register staging as its own site, or exclude its hostname with a shield.
If you have more than one base template
admin/base.html, dashboard/base.html - that doesn’t extend the first. Each one needs the tag, or one whole section of the site reports no visitors and looks like a content problem.Behind a proxy
If Django runs behind nginx, a load balancer or Cloudflare, make sure the visitor’s address is being forwarded. The ingestion health panel warns you when it isn’t - otherwise every visitor collapses into one and geolocates to your datacenter.
Server-side events - a webhook, a Celery task, an offline conversion - go through the Python SDK, which takes the visitor’s IP and user agent as required arguments, because a call without them looks like a datacenter bot.
Check it worked
Deploy, open the site in a normal browser window, and click through two or three pages. Then open Feasible.
You should show up under Real-time visitors. If you don’t, go to Site settings → Ingestion health. It counts every event that arrived and every one that was dropped, each with a named reason - unknown_site means the snippet is for a site you haven’t registered, hostname_not_allowed means the page is on a hostname that isn’t on your list.
The send a test event button there posts through the real public URL, so it exercises what a browser does.
A brand-new site takes about fifteen seconds before its first event is accepted, so give it that.
Related: Laravel, React, and the install docs.
Sources: Django - built-in template context processors, checked September 3, 2026.