Skip to content
Ranqo
Docs
DashboardGet started
GuidesMethodologyIntegrations
Video tutorials

Overview

  • Integrations

Analytics

  • Google Analytics 4

Publishing

  • WordPress

Site Tracking

  • Site Tracking
  • Next.js
  • Express
  • Cloudflare Worker
  • Any backend
  • Verify
  • Site keys
  • Intake API
  • Privacy

7 sections

Site Tracking

Site Tracking for any backend

View as MarkdownOpen this page as .md
ChatGPTOpen in ChatGPTAsk about this pageClaudeOpen in ClaudeAsk about this page

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:

  1. Fire after responding. Send the report once your response is on its way, never before.
  2. 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.
  3. Ignore the result, and never retry. Ranqo answers 200 with {"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.
  4. 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:

ParameterValue
urlThe full URL of the page requested, including the scheme and host.
userAgentThe request's User-Agent header, or empty.
refThe request's Referer header, or empty.
ipThe visitor's IP address, or empty. Behind a proxy or load balancer, use the first address in X-Forwarded-For.
websiteKeyYour 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-Dest header other than document, a Next-Router-Prefetch, Next-Router-Segment-Prefetch or RSC: 1 header, or a Sec-Purpose or Purpose header saying prefetch.
  • Paths under /api/.
  • Static files: images, fonts, scripts, stylesheets, source maps, and .txt, .xml and .json files, 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.

Any backend
# 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
# 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 response

Add 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.Handler wrapper 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.

Previous page: Cloudflare WorkerNext page: Verify
On this page
Ranqo
Docs
Dashboardranqo.aiPrivacyTerms
Be the Source AI Cites.
Ranqo
Docs
GuidesMethodologyIntegrations

Overview

  • Integrations

Analytics

  • Google Analytics 4

Publishing

  • WordPress

Site Tracking

  • Site Tracking
  • Next.js
  • Express
  • Cloudflare Worker
  • Any backend
  • Verify
  • Site keys
  • Intake API
  • Privacy
Get started