Privacy-first, cookie-free analytics engine for Manhali tenants. It runs on Cloudflare Workers, stores events in Analytics Engine, validates browser origins through KV, and exposes query APIs for the platform dashboard.
See ANALYTICS_ARCHITECTURE.md for the internal data model and flow details.
Browser ── GET /tracker.js?sid=... ───────────────→ Analytics Worker
Browser ── POST /api/collect ── writeDataPoint ─→ Analytics Engine
Backend ── POST /api/collect (Bearer + tid) ────→ Analytics Worker
Dashboard ── GET /api/* (admin/scoped token) ───→ Worker ── SQL API ──→ Analytics Engine
Admin ── /api/admin/domains , /api/admin/token ─→ Worker ── KV / HMAC ─→ DOMAINS_KV
Storage
- Analytics Engine: event storage and live analytics queries, with 90-day retention
- KV (
DOMAINS_KV): per-tenant domain allowlist used by browser collection
# Install dependencies
bun install
# Start local dev server
bun run dev
# → Wrangler dev (default: http://localhost:8787)
# Run tests
bun run test
# Deploy to Cloudflare
bun run deploy
# View real-time logs
bun run tail# KV namespace for per-tenant domain allowlists
wrangler kv namespace create DOMAINS_KVSet the KV namespace ID and your Cloudflare account ID.
[[kv_namespaces]]
binding = "DOMAINS_KV"
id = "your-kv-namespace-id"
[vars]
ACCOUNT_ID = "your-account-id"
ENVIRONMENT = "production"Optional runtime vars:
[vars]
ALLOW_UNREGISTERED_DOMAINS = "true" # optional setup grace period for browser collectwrangler secret put AUTH_TOKEN # admin API token + signing secret for scoped tokens
wrangler secret put CF_API_TOKEN # Cloudflare API token for Analytics Engine SQL queries
wrangler secret put FINGERPRINT_SECRET # dedicated visitor-hash secret
wrangler secret put INGEST_TOKEN # optional dedicated token for server-to-server ingestionbun run deployBrowser-origin collection expects domains to be registered per tenant in production.
curl -X POST "https://analytics.example.com/api/admin/domains?tenant_id=TENANT_ID" \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"domain": "tenant-site.com"}'Use the admin token to mint a tenant-scoped token for dashboards or internal services that should only read one tenant's analytics.
curl -X POST "https://analytics.example.com/api/admin/token" \
-H "Authorization: Bearer YOUR_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"tenant_id":"TENANT_ID","ttl":86400}'Add the tracker script to any tenant page:
<script
src="https://analytics.example.com/tracker.js?sid=TENANT_SITE_ID"
defer
></script>The tracker automatically:
- Sends a
pageviewevent on initial load - Sends a
beaconevent with time-on-page onvisibilitychangeandpagehide - Tracks SPA navigation via
pushState,replaceState,popstate, andhashchange - Extracts UTM parameters from the page URL
- Supports custom events via
window.manhali.track("event_name") - Supports manual pageview dispatch via
window.manhali.pageview()
| Method | Path | Description |
|---|---|---|
GET |
/health |
Health check |
GET |
/tracker.js?sid=SITE_ID |
Parameterized tracker script |
POST |
/api/collect |
Browser or server-to-server event collection |
Query routes require Authorization: Bearer <token> and ?tenant_id=.
- Admin token: unrestricted access
- Scoped token: restricted to one tenant only
| Method | Path | Query Params | Description |
|---|---|---|---|
GET |
/api/overview |
days (1-90, default 30) |
Totals plus top pages summary |
GET |
/api/pages |
days, limit (1-100, default 20) |
Top pages by views |
GET |
/api/referrers |
days, limit |
Top referrer domains plus UTM data |
GET |
/api/geo |
days |
Country and city breakdown |
GET |
/api/devices |
days |
Desktop / Mobile / Tablet breakdown |
GET |
/api/realtime |
— | Visitors active in the last 30 minutes |
GET |
/api/events |
days, limit |
Custom event totals and unique visitors |
Admin routes require the admin AUTH_TOKEN. Scoped tenant tokens are rejected.
| Method | Path | Query Params | Description |
|---|---|---|---|
GET |
/api/admin/domains |
tenant_id |
List allowed domains for a tenant |
POST |
/api/admin/domains |
tenant_id |
Add domain {"domain":"example.com"} |
DELETE |
/api/admin/domains |
tenant_id |
Remove domain {"domain":"example.com"} |
POST |
/api/admin/token |
— | Create a tenant-scoped HMAC token |
Tenant resolution priority on collect:
tid+ valid Bearer token for server-to-server ingestionsid+ allowed browser origin for tracker requests- Development fallback to the request origin hostname when
ENVIRONMENT=development
| Binding | Type | Purpose |
|---|---|---|
ANALYTICS |
Analytics Engine | Event storage via writeDataPoint() |
DOMAINS_KV |
KV | Per-tenant domain allowlist |
| Variable | Source | Purpose |
|---|---|---|
AUTH_TOKEN |
Secret | Admin API token and scoped-token signing secret |
CF_API_TOKEN |
Secret | Cloudflare API token for Analytics Engine SQL queries |
FINGERPRINT_SECRET |
Secret | Dedicated secret for privacy-safe visitor hashing |
INGEST_TOKEN |
Secret | Optional separate token for server-to-server ingestion |
ACCOUNT_ID |
Var | Cloudflare account ID |
ENVIRONMENT |
Var | development or production |
ALLOW_UNREGISTERED_DOMAINS |
Var | Optional "true" grace period for unregistered site domains |
{ "t": "pageview", // "pageview" | "beacon" | "event" "p": "/courses/intro", // page path (required) "r": "https://google.com", // document.referrer "d": 42, // duration in seconds (beacon) "sid": "tenant_123", // browser tracker site ID "tid": "tenant_123", // server-to-server tenant ID (requires Bearer auth) "en": "signup_click", // custom event name "us": "google", // utm_source "um": "cpc", // utm_medium "uc": "spring_sale", // utm_campaign "co": "EG", // optional country override, server-to-server only "ci": "Cairo", // optional city override, server-to-server only }