# 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.

Source: https://ranqo.ai/docs/integrations/site-tracking/any-backend

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:

| 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](https://ranqo.ai/docs/integrations/site-tracking/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.

```bash
# 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.

```python
# 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](https://ranqo.ai/docs/integrations/site-tracking/verify), then load a page on your site and watch the install page in the dashboard.
