Site Tracking
Site Tracking for any backend
Add Site Tracking to any server that can make an HTTP request, from Python and Ruby to Go and PHP, with the rules every install must follow.
Site Tracking needs one thing from your server: after it answers a page request, it sends a GET request to Ranqo describing that page view. Any language or framework that can make an outbound HTTP request can do it. If you use Next.js, Express or a Cloudflare Worker, their guides have ready-made code.
The contract
Every install, in any language, follows four rules:
- Fire after responding. Send the report once your response is on its way, never before.
- Never block. Send it in the background (a thread, a goroutine, a queued job) and do not wait for the answer. Use a short timeout.
- Ignore the result, and never retry. Ranqo answers
200with{"ok": true}or{"ok": false}, and a failure on its side can return an error status instead. None of these needs handling. Every accepted report is recorded as a new visit, so retrying one counts the page view twice. - Report page views only. Skip the requests the install code for every stack skips (below).
What to send
One GET request to the intake endpoint with five query parameters, URL-encoded:
| Parameter | Value |
|---|---|
url | The full URL of the page requested, including the scheme and host. |
userAgent | The request's User-Agent header, or empty. |
ref | The request's Referer header, or empty. |
ip | The visitor's IP address, or empty. Behind a proxy or load balancer, use the first address in X-Forwarded-For. |
websiteKey | Your site key, read from the RANQO_SITE_KEY environment variable. |
The Intake API page covers the endpoint in full, including its limits.
What to skip
- Any method other than
GET. - Prefetch and framework data requests: a
Sec-Fetch-Destheader other thandocument, aNext-Router-Prefetch,Next-Router-Segment-PrefetchorRSC: 1header, or aSec-PurposeorPurposeheader sayingprefetch. - Paths under
/api/. - Static files: images, fonts, scripts, stylesheets, source maps, and
.txt,.xmland.jsonfiles, except the ones below. - Hosts you do not want counted, such as your signed-in product if the same server runs it.
Always report robots.txt, llms.txt, llms-full.txt, sitemap.xml and sitemap-*.xml, ai.txt and anything under /.well-known/. They are the first files AI crawlers ask for.
From the command line
The same request as a shell command, run in the background. Replace the variables with your framework's values.
# After your handler returns, fire this in the background — never block the user:
curl -sG "https://app.ranqo.ai/api/v1/intake/pageview" \
--data-urlencode "url=$REQUEST_URL" \
--data-urlencode "userAgent=$REQUEST_USER_AGENT" \
--data-urlencode "ref=$REQUEST_REFERER" \
--data-urlencode "ip=$CLIENT_IP" \
--data-urlencode "websiteKey=$RANQO_SITE_KEY" \
>/dev/null 2>&1 &Python
A Django middleware that applies every rule above and sends the report from a background thread with a half-second timeout. The same shape works in Flask, FastAPI and other frameworks: wrap the response, then fire the request in the background.
# ranqo_intake.py — Django middleware. Adapt to Flask, FastAPI, etc.
import os, re, threading
from urllib.parse import urlencode
import urllib.request
SKIP_EXT = re.compile(r"\.(png|jpg|jpeg|svg|gif|webp|ico|css|js|mjs|map|woff2?|ttf|otf|eot|txt|xml|json)$", re.I)
TRACK_AI = re.compile(r"^/(robots\.txt|llms(-full)?\.txt|sitemap(-.*)?\.xml|ai\.txt|\.well-known/.*)$")
INTAKE = "https://app.ranqo.ai/api/v1/intake/pageview"
# OPTIONAL: skip self-tracking on subdomains you don't want counted as
# "marketing site visitors" (e.g. your authenticated dashboard).
SKIP_HOSTS = set([
# "app.yoursite.com",
# "admin.yoursite.com",
])
def _fire(params):
try:
req = urllib.request.Request(INTAKE + "?" + urlencode(params))
urllib.request.urlopen(req, timeout=0.5).read()
except Exception:
pass # fire-and-forget
class RanqoIntakeMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# Only GET is a page view. Skip non-GET and any prefetch / RSC
# request (Next.js apps behind a Python proxy would otherwise inflate).
if request.method != "GET":
return response
sec_fetch_dest = request.META.get("HTTP_SEC_FETCH_DEST", "")
if sec_fetch_dest and sec_fetch_dest != "document":
return response
if request.META.get("HTTP_NEXT_ROUTER_PREFETCH"):
return response
if request.META.get("HTTP_NEXT_ROUTER_SEGMENT_PREFETCH"):
return response
if request.META.get("HTTP_RSC") == "1":
return response
sec_purpose = request.META.get("HTTP_SEC_PURPOSE", "")
if sec_purpose.startswith("prefetch"):
return response
if request.META.get("HTTP_PURPOSE") == "prefetch":
return response
path = request.path
host = request.get_host()
if host in SKIP_HOSTS:
return response
if not path.startswith("/api/") and (TRACK_AI.match(path) or not SKIP_EXT.search(path)):
params = {
"url": request.build_absolute_uri(),
"userAgent": request.META.get("HTTP_USER_AGENT", ""),
"ref": request.META.get("HTTP_REFERER", ""),
"ip": (request.META.get("HTTP_X_FORWARDED_FOR", "") or "").split(",")[0].strip(),
"websiteKey": os.environ.get("RANQO_SITE_KEY", ""),
}
threading.Thread(target=_fire, args=(params,), daemon=True).start()
return responseAdd the class to your MIDDLEWARE setting by its module path (for example ranqo_intake.RanqoIntakeMiddleware) and set RANQO_SITE_KEY in the environment. The example reads the visitor's address only from X-Forwarded-For. If your app is not behind a proxy that sets it, use request.META["REMOTE_ADDR"] instead, or no address is sent and bot visits cannot be verified.
Other languages
Translate the Python middleware directly: the same checks, the same five parameters, and a background request with a short timeout.
- Ruby: a Rack middleware that calls the app, then sends the request from a thread.
- Go: an
http.Handlerwrapper that calls the next handler, then starts a goroutine for the request. - PHP: send the request after the response has been flushed to the visitor, so the page is not held up.
Whatever the language, never let an error from the report reach the visitor.
Check it
Run the check in Verify your install, then load a page on your site and watch the install page in the dashboard.