Skip to content

Lightweight, cookie-free analytics for services behind Traefik. Injects tracking via response body rewriting, stores events in MongoDB, and serves a static dashboard. Zero changes to application code.

Notifications You must be signed in to change notification settings

meng2468/traefik-analytics

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Self-Hosted Analytics System

Lightweight, cookie-free analytics for all services behind Traefik. Tracks page views per subdomain using browser fingerprinting, SPA route detection, and sendBeacon. Data stored in MongoDB, viewable via a static HTML dashboard.

Architecture

Browser ──▶ Cloudflare ──▶ Traefik (ports 80/443, host network)
                              │
                              ├─ rewritebody plugin injects <script> into HTML responses
                              │
                              ├─ /m/s.js ──▶ analytics backend (port 4000) ──▶ serves script
                              ├─ /m/e    ──▶ analytics backend (port 4000) ──▶ stores event in MongoDB
                              └─ /*      ──▶ your service (port N)

Each tracked subdomain has two Traefik routers:

  1. High priority (/m/ prefix) — routes to the analytics backend
  2. Low priority (catch-all) — routes to the actual service, with the rewritebody middleware attached

Components

Component Location Purpose
public/s.js Served at /m/s.js Frontend analytics script (fingerprinting, SPA tracking, sendBeacon)
backend/server.py Port 4000 FastAPI backend (receives beacons, serves script, dashboard API)
public/dashboard.html Served at / on analytics.maltemeng.com Static HTML dashboard with host filtering
docker-compose.yml /root/analytics/ Runs backend + MongoDB
Traefik route files /root/traefik/routes/*.yml Per-service routing + script injection

Data Collected

Each page view event stores:

Field Type Source Description
hostname string location.hostname Subdomain the event came from
path string location.pathname + search Current page URL path
referrer string document.referrer Page that linked the visitor here
visitorId string djb2 hash Fingerprint (see below)
clientTs int Date.now() Client-side timestamp (ms since epoch)
serverTs datetime server Server-side UTC timestamp on receipt
screenWidth int screen.width Screen width in pixels
screenHeight int screen.height Screen height in pixels
ip string x-forwarded-for header Visitor IP (from Traefik/Cloudflare)
userAgent string request header Browser user-agent string

Fingerprint

No cookies are used. The visitor ID is a djb2 hash of these browser properties:

Property Source
Language navigator.language
Screen size screen.width + 'x' + screen.height
Color depth screen.colorDepth
Timezone Intl.DateTimeFormat().resolvedOptions().timeZone
CPU cores navigator.hardwareConcurrency
Platform navigator.platform

SPA Route Tracking

The script monkey-patches history.pushState and history.replaceState and listens for popstate events to capture SPA navigation without touching application code. Uses navigator.sendBeacon for reliable delivery that survives page unloads.

API Endpoints

All served by the analytics backend on port 4000:

Method Path Description
GET /m/s.js Analytics script (cached 24h)
POST /m/e Beacon receiver (returns 204)
GET /m/dashboard?key=SECRET Aggregated stats JSON. Optional &host= filter
GET /m/recent?key=SECRET Last 50 events JSON. Optional &host= filter
GET /m/health Health check
GET / Dashboard HTML (served on analytics.maltemeng.com)

Ad Blocker Avoidance

All public-facing paths use generic names to avoid filter lists:

  • Script: /m/s.js (not analytics.js, tracking.js, etc.)
  • Beacon: /m/e (not /api/analytics, /beacon, etc.)
  • Same-origin: script and beacon served from the same subdomain as the app
  • No third-party domains, no known library filenames

Adding a New Service

When you have a new service to track (e.g. app.maltemeng.com on port 5000), only two things are needed:

Step 1: Create a Traefik route file

Create /root/traefik/routes/app.yml:

http:
  routers:
    app-metrics:
      rule: "Host(`app.maltemeng.com`) && PathPrefix(`/m/`)"
      entryPoints:
        - websecure
      service: app-analytics
      priority: 100
      tls:
        certResolver: letsencrypt

    app:
      rule: "Host(`app.maltemeng.com`)"
      entryPoints:
        - websecure
      service: app
      priority: 50
      tls:
        certResolver: letsencrypt
      middlewares:
        - app-inject-script

  middlewares:
    app-inject-script:
      plugin:
        rewritebody:
          rewrites:
            - regex: "</head>"
              replacement: '<script defer src="/m/s.js"></script></head>'

  services:
    app:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1:5000"

    app-analytics:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1:4000"

What this does:

  • app-metrics router (priority 100) catches /m/* requests and routes them to the analytics backend on port 4000, so the script and beacon endpoint are reachable
  • app router (priority 50) catches everything else and forwards to your service, with the rewritebody middleware injecting the <script> tag before </head>
  • Middleware and service names must be unique across all route files (prefix with the app name)

Traefik's file watcher picks this up automatically — no restart needed.

Step 2: Create the DNS record (if new subdomain)

Use the Cloudflare DNS service on this server:

curl -X POST "http://127.0.0.1:8000/api/zones/9f39f5582e0d1da2a53eba2f1ce6f8da/records" \
  -H "Content-Type: application/json" \
  -d '{"type":"A","name":"app","content":"87.106.219.33","proxied":true,"ttl":1}'

The zone ID 9f39f5582e0d1da2a53eba2f1ce6f8da is for maltemeng.com. The server IP is 87.106.219.33. Set proxied: true to route through Cloudflare.

That's it

Nothing else changes. The analytics backend, script, dashboard, MongoDB, and traefik.yml are all shared. The new hostname will appear automatically in the dashboard host filter once events start arriving.


How Script Injection Works

Three pieces work together:

1. Plugin registration (static config, one-time)

In /root/traefik/traefik.yml, the plugin-rewritebody is registered under experimental.plugins. This tells Traefik to download and load the Go plugin at startup:

experimental:
  plugins:
    rewritebody:
      moduleName: "github.com/traefik/plugin-rewritebody"
      version: "v0.3.1"

2. Middleware definition (per route file)

Each route file defines a middleware that uses the plugin to regex-replace </head> in the response body:

middlewares:
  app-inject-script:
    plugin:
      rewritebody:
        rewrites:
          - regex: "</head>"
            replacement: '<script defer src="/m/s.js"></script></head>'

Important: The key under plugin: must be rewritebody (matching the name in static config), not the middleware name.

3. Middleware applied to router

The middleware is referenced on the router:

routers:
  app:
    middlewares:
      - app-inject-script

At runtime: Traefik forwards the request to your service → your service returns HTML → the rewritebody middleware intercepts the response, finds </head>, injects the script tag → the modified HTML reaches the browser.

Gotchas

  • Plugin name mismatch: The key under plugin: in the middleware definition must exactly match the plugin name in traefik.yml (rewritebody). Using a different name (e.g. rewrite-body) causes "unknown plugin type" errors.
  • Static config changes require restart: Modifying traefik.yml (e.g. adding the plugin) requires docker compose restart in /root/traefik/. Route file changes are hot-reloaded.
  • Cloudflare caching: After adding injection, Cloudflare may serve cached HTML without the script tag. Purge the cache from the Cloudflare dashboard or wait for TTL expiry.
  • Compression: If a service compresses responses (gzip/brotli), the rewritebody plugin handles gzip transparently. For brotli, you may need to disable compression on the service side (e.g. compress: false in Next.js config).
  • Unique names: Middleware and service names must be globally unique across all route files. Prefix with the app name (e.g. app-inject-script, app-analytics).
  • CORS: The dashboard at analytics.maltemeng.com fetches data from maltemeng.com/m/dashboard. CORS is configured in the backend to allow analytics.maltemeng.com and maltemeng.com origins.

Running

# Start analytics backend + MongoDB
cd /root/analytics
docker compose up -d --build

# Restart Traefik (only needed after traefik.yml changes)
cd /root/traefik
docker compose restart

Dashboard

Accessible at https://analytics.maltemeng.com. Protected by DASHBOARD_SECRET env var (set in /root/analytics/docker-compose.yml, default: changeme).

Features:

  • Total views, unique visitors, daily average
  • Daily views bar chart
  • Traffic by host table (click to filter)
  • Top pages and referrers
  • Recent events feed
  • Host filter buttons to drill into per-subdomain data
  • Full endpoint and data collection documentation

File Locations

/root/analytics/                    # Analytics repo
├── docker-compose.yml              # Backend + MongoDB containers
├── public/
│   ├── s.js                        # Frontend analytics script
│   └── dashboard.html              # Dashboard UI
└── backend/
    ├── server.py                   # FastAPI application
    ├── requirements.txt            # Python dependencies
    └── Dockerfile

/root/traefik/                      # Traefik config
├── traefik.yml                     # Static config (includes rewritebody plugin)
├── docker-compose.yml              # Traefik container
└── routes/
    ├── homepage.yml                # maltemeng.com (with injection middleware)
    ├── dns.yml                     # dns.maltemeng.com (with injection middleware)
    ├── analytics.yml               # maltemeng.com/m/* → analytics backend
    ├── dashboard.yml               # analytics.maltemeng.com → analytics backend
    └── example.yml                 # Template (not active)

About

Lightweight, cookie-free analytics for services behind Traefik. Injects tracking via response body rewriting, stores events in MongoDB, and serves a static dashboard. Zero changes to application code.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors