Here’s a clean, copy-pasteable usage guide for your containerized FastAPI visitor tracker (with Web UI, stats, CSV, and an invisible pixel).


Visitor Tracker — Usage Guide

0) Prereqs


1) Install & Run

unzip visitor-tracker-with-ui.zip
cd visitor-tracker
docker compose up --build

What this does:

Health check:

curl http://localhost:8000/healthz
# {"status":"ok"}

2) Endpoints (overview)

Endpoint Method Purpose
/pixel GET Logs a visit (returns 1×1 GIF). For invisible image/fetch embeds.
/collect POST Alternative logging endpoint (accepts JSON body).
/tags GET Returns known tags & counts.
/stats GET Returns bucketed time-series (hour/day/week/month) with filters.
/visits.json GET Paginated table API (filters + search + sort).
/visits.csv GET CSV export for current filters.
/dashboard GET Web UI: interactive Chart.js time-series.
/visits GET Web UI: paginated table + CSV download.
/healthz GET Health probe.

3) Embed (invisible, non-blocking)

Production-safe (fire & forget, with client-side timeout)

Replace TRACK_DOMAIN and TAG.

<script>
(function () {
  var url = "https://TRACK_DOMAIN/pixel?t=TAG&u=" + encodeURIComponent(location.href);

  if (window.fetch && window.AbortController) {
    var c = new AbortController();
    setTimeout(function(){ try{ c.abort(); }catch(e){} }, 1000);
    fetch(url, { mode: "no-cors", cache: "no-store", keepalive: false, signal: c.signal })
      .catch(function(){});
    return;
  }

  var img = new Image();
  img.decoding = "async";
  img.loading = "eager";
  img.src = url;
})();
</script>
<noscript>
  <img src="https://TRACK_DOMAIN/pixel?t=TAG" width="1" height="1" style="display:none" alt="" />
</noscript>

Local testing: use http://localhost:8000 as TRACK_DOMAIN. HTTPS sites: don’t mix https pages with http tracker—browsers will block mixed content.


4) Quick Tests with curl

⚠️ Use GET (do not use -I/HEAD). HEAD returns 405 by design.

4.1 Seed some visits

# Home
curl "http://localhost:8000/pixel?t=home&u=http%3A%2F%2Flocalhost%2F"

# Pricing with referrer and spoofed client IP
curl -H "Referer: https://google.com" -H "X-Forwarded-For: 203.0.113.77" \
  "http://localhost:8000/pixel?t=pricing&u=http%3A%2F%2Flocalhost%2Fpricing"

# Blog
curl "http://localhost:8000/pixel?t=blog&u=http%3A%2F%2Flocalhost%2Fblog%2Fpost-42"

4.2 Verify data

curl "http://localhost:8000/tags"
# e.g. [{"tag":"home","count":1},{"tag":"pricing","count":1},{"tag":"blog", "count":1}]

curl "http://localhost:8000/visits.json"
# {"page":1,"page_size":50,"total":3,"items":[...]}

4.3 Stats (time-series)

# All tags, daily buckets
curl "http://localhost:8000/stats?interval=day"

# Only 'pricing' for a range (inclusive)
curl "http://localhost:8000/stats?interval=hour&tags=pricing&start=2025-08-01T00:00:00&end=2025-08-31T23:59:59"

4.4 Table API (filters, paging, search)

# Newest first (default), 50 per page
curl "http://localhost:8000/visits.json?page=1&page_size=50"

# Filter by tags and date
curl "http://localhost:8000/visits.json?tags=home&tags=pricing&start=2025-08-01&end=2025-08-31"

# Text search in url/referrer/user-agent
curl "http://localhost:8000/visits.json?search=google"

4.5 CSV export

curl -o visits.csv "http://localhost:8000/visits.csv?tags=pricing"
# Open visits.csv in Excel/Sheets

5) Web UI


6) API Details

6.1 /pixel (GET)

Query:

Response: 1×1 GIF (image/gif), cache-disabled.

6.2 /collect (POST)

6.3 /stats (GET)

Query:

Response:

{
  "interval": "day",
  "tags": ["pricing"],
  "series": [
    {"bucket":"2025-08-20","count":12},
    ...
  ]
}

6.4 /visits.json (GET)

Query:

Response:

{
  "page":1,
  "page_size":50,
  "total":123,
  "items":[
    {
      "id": 42,
      "ts": "2025-08-20T03:40:12.123456",
      "page_tag": "pricing",
      "url": "https://mysite.com/pricing",
      "referrer": "https://google.com",
      "ip": "203.0.113.77",
      "user_agent": "Mozilla/5.0 ...",
      "accept_lang": "en-US,en;q=0.9"
    }
  ]
}

6.5 /visits.csv (GET)

Same filters as /visits.json; returns a CSV file.


7) Gotchas & Troubleshooting

“405 Method Not Allowed” on /pixel

You likely used curl -I (HEAD). The route is GET only. Use curl "http://.../pixel?... (no -I).

“sqlite3.OperationalError: unable to open database file”

SQLite needs a directory that exists & is writable. The provided docker-compose.yml mounts a directory and uses:

DATABASE_URL=sqlite:////app/data/visits.db

If you changed it, ensure the parent folder exists or revert to the compose defaults.

No data appears in /dashboard or /visits

Mixed content blocked in browser

Your site is HTTPS, but pixel is HTTP (localhost). Use HTTPS in production (reverse proxy like Caddy/Nginx with Let’s Encrypt).

Behind a proxy (real client IP)

Forward X-Forwarded-For or X-Real-IP. The app already reads both and falls back to request.client.host.


8) Switching to Postgres (optional)

Compose services (example)

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: tracker
      POSTGRES_PASSWORD: trackerpass
      POSTGRES_DB: tracker
    volumes:
      - pg_data:/var/lib/postgresql/data
  tracker:
    build: .
    depends_on: [db]
    ports: ["8000:8000"]
    environment:
      DATABASE_URL: "postgresql+psycopg://tracker:trackerpass@db:5432/tracker"
volumes:
  pg_data:

The app already uses date_trunc on Postgres for bucketing; no code changes required.


9) Data Model (SQLite/Postgres)

visits table


10) Security & Privacy Notes


11) One-liners you’ll reuse

Seed a lot of visits quickly:

for i in $(seq 1 10); do curl -s "http://localhost:8000/pixel?t=home&u=http%3A%2F%2Flocalhost%2F" >/dev/null; done

Newest 50 rows:

curl "http://localhost:8000/visits.json?page=1&page_size=50"

Only “pricing” this month:

curl "http://localhost:8000/visits.json?tags=pricing&start=$(date -u +%Y-%m-01T00:00:00Z)&end=$(date -u +%Y-%m-%dT23:59:59Z)"

CSV of last 7 days:

START=$(date -u -v-7d +%Y-%m-%d 2>/dev/null || date -u -d '-7 days' +%F)
curl -o visits_last7.csv "http://localhost:8000/visits.csv?start=$START"

If you want, I can add quick presets on /dashboard (Last 24h / 7d / 30d buttons), Basic Auth for the UI, and IST bucketing (tz_offset_minutes=330).