Skip to content

Repository files navigation

Flarelytics

CI License: MIT Docs

Privacy-first web analytics that runs entirely on Cloudflare. No cookies, no external dependencies, 5-minute setup.

Website: flarelytics.dev

Why Flarelytics?

  • 100% Cloudflare — Workers + Analytics Engine. No databases, no servers, no third-party services.
  • Privacy by architecture — No cookies. No fingerprinting. Daily-rotating visitor hash that resets every 24 hours. GDPR/CCPA compliant without a cookie banner.
  • Under 2KB gzipped — The tracking script is smaller than most cookie consent popups.
  • Custom events — Track signups, purchases, clicks, or anything else with flarelytics.track('event', { props }).
  • Scroll depth — Optional IntersectionObserver-based tracking at 25/50/75/100% milestones.
  • Email reports — Weekly digests with traffic trends, top pages, and anomaly alerts.
  • Open source — MIT licensed. Self-host on your own Cloudflare account.

Quick Start (5 minutes)

1. Deploy the worker

git clone https://github.com/kalle-works/flarelytics.git
cd flarelytics && npm install
cd packages/worker

# Edit wrangler.toml with your account_id and allowed origins
npm run deploy      # builds the tracker bundle, then wrangler deploy
npx wrangler secret put QUERY_API_KEY    # random string for dashboard auth
npx wrangler secret put CF_API_TOKEN     # CF API token (Analytics Engine read)
npx wrangler secret put CF_ACCOUNT_ID    # your CF account ID

Quick setup: Run bash setup.sh in the worker directory for an interactive guided setup that configures everything automatically.

2. Add the tracking script

<!-- Basic tracking -->
<script defer data-endpoint="https://your-worker.workers.dev" src="/tracker.js"></script>

<!-- With scroll depth tracking -->
<script defer data-endpoint="https://your-worker.workers.dev" data-scroll-depth src="/tracker.js"></script>

Pageviews, outbound link clicks, and time-on-page are tracked automatically, including client-side navigation in single-page apps (React Router, Vue Router, Astro view transitions, etc.) — a route change fires a pageview without a full page reload. The tracker also skips sending anything while running on localhost/127.0.0.1/file://, so a plain npm run dev session doesn't send test traffic into production. Both are configurable via script-tag attributes:

<!-- Disable automatic SPA pageview tracking -->
<script defer data-endpoint="https://your-worker.workers.dev" data-no-spa src="/tracker.js"></script>

<!-- Send events even on localhost (useful when testing against a real worker) -->
<script defer data-endpoint="https://your-worker.workers.dev" data-allow-localhost src="/tracker.js"></script>

Or with npm:

npm install @flarelytics/tracker
import { init, track } from '@flarelytics/tracker'

// Basic
init('https://your-worker.workers.dev')

// With scroll depth tracking at 25/50/75/100% milestones
init('https://your-worker.workers.dev', { scrollDepth: true })

// Disable SPA navigation tracking, or send events on localhost
init('https://your-worker.workers.dev', { noSpa: true, allowLocalhost: true })

// Track custom events
track('signup', { props: { plan: 'pro' } })

3. Verify it works

# Check the worker is running
curl https://your-worker.workers.dev/health
# → { "status": "healthy", "checks": { "analytics_binding": true } }

# Visit your site, then check data is flowing (wait ~30 seconds)
curl -H "X-API-Key: your-api-key" \
  "https://your-worker.workers.dev/query?q=daily-views&period=7d&site=yoursite.com"
# → { "data": [{ "date": "2026-04-12", "views": 1 }] }

4. View your dashboard

Visit the dashboard and sign in with SSO — you'll see the sites your organization owns, with an org switcher when you belong to more than one.

The dashboard talks to a single worker (set PUBLIC_API_BASE at build time, or append ?worker=https://your-worker.workers.dev for local/self-hosted use). Programmatic access still uses the X-API-Key header (see the API examples below); the browser dashboard no longer stores any key.

Architecture

packages/
  worker/         Cloudflare Worker: event tracking + query API
  tracker/        Client-side script (under 2KB gzipped): pageviews, outbound links, custom events, scroll depth
  dashboard/      Astro static site: analytics dashboard with charts and tables
  email-reports/  Cloudflare Worker cron: weekly email digests
  landing/        Astro static site: flarelytics.dev marketing + docs

Event Types

Event Tracked automatically Description
pageview Yes Page load with referrer and UTM params
outbound Yes External link clicks
timing Yes Time on page in seconds (fires on visibilitychange)
scroll_depth Opt-in Scroll milestones at 25/50/75/100%
bot_hit Yes Bot traffic recorded separately (UA in blob5)
(custom) Manual Any event via flarelytics.track()

API

Track events

POST /track
Content-Type: application/json

{
  "event": "pageview",
  "path": "/pricing",
  "referrer": "google.com",
  "utm_source": "newsletter"
}

Custom events

POST /track

{
  "event": "signup",
  "path": "/pricing",
  "props": { "plan": "pro", "source": "hero-cta" }
}

Query analytics

GET /query?q=<query-name>&period=30d&site=yoursite.com
X-API-Key: your-api-key

Periods: 7d, 14d, 30d, 60d, 90d, 180d

Available queries

Traffic

Query Params Description
top-pages Most viewed pages
top-pages-visitors Top pages with views and unique visitor counts
top-pages-stories Top pages where path starts with /a/
daily-views Pageviews per day
daily-unique-visitors Unique visitors per day (+ total views)
total-sessions Total sessions in period. A session is a timing event, which only fires after >1s of dwell time — pages left immediately aren't counted
total-pageviews Total pageviews in period
total-visitors Total unique visitors in period

Referrers & Acquisition

Query Params Description
referrers Top referrer hostnames
referrers-by-page ?page=/path Referrer breakdown for a specific page
utm-campaigns UTM campaign totals (source, medium, campaign)
utm-campaign-trend Daily UTM visits — when each post drove traffic
utm-by-page ?page=/path UTM campaign breakdown for a specific page

Content & Engagement

Query Params Description
page-views-over-time ?page=/path Daily views + visitors for one page
page-timing Average time on page in seconds per path
timing-by-page ?page=/path Average time on page for a specific page
bounce-rate-by-page ?event_name=N Bounce % per page (threshold seconds, default 10). Counts timing events (sessions with >1s dwell), so pages left within 1s aren't in the denominator at all
scroll-depth Scroll depth distribution: how far visitors scroll across all pages
scroll-depth-by-page Scroll depth breakdown per page
scroll-depth-for-page ?page=/path Scroll depth distribution for a specific page

Geography & Devices

Query Params Description
countries Views by country
countries-by-page ?page=/path Country breakdown for one page
devices Pageviews by device type (mobile/tablet/desktop)
browsers Pageviews by browser
operating-systems Pageviews by operating system (Windows/macOS/iOS/Android/Linux/ChromeOS)

Revenue

Query Params Description
revenue-by-event Events with a revenue value — total, count, average (requires value in /track calls)
revenue-over-time Daily revenue totals and conversion counts

Conversions

Query Params Description
outbound-links External link click destinations
page-performance Pages with views vs custom event CTR
custom-events Custom event counts by name and properties
conversion-funnel Daily pageviews to custom events
funnel-by-event ?event_name=signup Daily funnel for a specific custom event

Live (30-minute window)

Query Params Description
live-visitors Visitors and pageviews in the last 30 minutes
live-pages Most visited pages in the last 30 minutes
live-referrers Top referrers in the last 30 minutes
hourly-today Pageviews by hour for the last 24 hours

Bot Reporting

Query Params Description
bot-hits Top bot user-agents
bot-hits-total Total bot hit count for the period
bot-pages Pages most targeted by bots
bot-daily Bot hits per day (trend)
bot-countries Countries where bot traffic originates

Example: scroll depth per page

GET /query?q=scroll-depth-by-page&period=30d&site=yoursite.com
X-API-Key: your-api-key
{
  "data": [
    { "path": "/a/my-article", "depth": "25", "count": 142 },
    { "path": "/a/my-article", "depth": "50", "count": 98 },
    { "path": "/a/my-article", "depth": "75", "count": 61 },
    { "path": "/a/my-article", "depth": "100", "count": 34 }
  ]
}

Example: UTM campaign trend

GET /query?q=utm-campaign-trend&period=30d&site=yoursite.com
X-API-Key: your-api-key
{
  "data": [
    { "date": "2026-04-08", "utm_source": "bluesky", "utm_campaign": "post-abc123", "visits": 47 },
    { "date": "2026-04-09", "utm_source": "bluesky", "utm_campaign": "post-abc123", "visits": 12 }
  ]
}

Example: bounce rate with custom threshold

GET /query?q=bounce-rate-by-page&period=30d&site=yoursite.com&event_name=30
X-API-Key: your-api-key
{
  "data": [
    { "path": "/", "bounced": 120, "sessions": 340, "bounce_pct": 35.3 },
    { "path": "/pricing", "bounced": 45, "sessions": 210, "bounce_pct": 21.4 }
  ]
}

Public stats (no API key required)

GET /public-stats?site=yoursite.com

Returns a summary of the last 30 days: pageviews, visitors, top pages, referrers, countries, devices, daily views, and bot hit total. Useful for public analytics pages.

{
  "period": "30d",
  "site": "yoursite.com",
  "stats": {
    "pageviews": 1234,
    "visitors": 567,
    "topPages": [{ "path": "/", "views": 300 }],
    "referrers": [{ "referrer": "google.com", "visits": 120 }],
    "countries": [{ "country": "FI", "views": 400 }],
    "devices": [{ "device": "desktop", "views": 800 }],
    "dailyViews": [{ "date": "2026-04-12", "views": 45 }],
    "botHitsTotal": 89
  }
}

Health check

GET /health
{ "status": "healthy", "checks": { "analytics_binding": true, ... }, "version": "0.2.0" }

Versioned queries (v1)

The v0 queries above return the raw Cloudflare Analytics Engine envelope ({ data: [...] }). The v1 family is a separate, structured query surface that aggregates multiple parallel SQL calls inside the worker and returns a strongly typed payload. v1 queries read from the per-family v1 datasets (pageview/engagement/share) introduced during the schema migration.

Dispatch by adding v=1:

GET /query?v=1&q=loop-overview&site=yoursite.com&period=30d
X-API-Key: your-api-key

Available v1 queries (also discoverable via GET /configqueries_v1):

Query Description
loop-overview Distribution Loop view: shares → social inbound → engaged reads → quality score, surfaced at canonical_url_hash level

Example response (loop-overview):

{
  "period": "'30' DAY",
  "site": "yoursite.com",
  "partial": false,
  "status": {
    "shares": "ok", "pageviews": "ok", "paths": "ok",
    "engagement": "ok", "socialInbound": "ok", "sharesTotal": "ok"
  },
  "kpis": {
    "articles_driving_shares": 47,
    "inbound_visits_from_social": 2341,
    "secondary_share_rate": 11.66,
    "avg_distribution_quality_score": 6.8
  },
  "articles": [
    {
      "canonical_url_hash": "a1b2c3d4e5f6",
      "path": "/articles/breaking",
      "first_seen": "2026-05-01T10:00:00Z",
      "shares_out": 125,
      "inbound_visits": 846,
      "engaged_reads": 488,
      "quality_score": 58
    }
  ]
}

When one or more underlying SQL buckets fails, the worker still returns 200 with partial: true and the affected KPIs as null (the dashboard renders ). The v0 query path is untouched — omit v=1 and you get the v0 contract.

Privacy

Flarelytics collects only what you need to understand your traffic:

Collected Why
Page path Know which pages are visited
Referrer hostname Know where traffic comes from
Country (from CF headers) Geographic distribution
Daily visitor hash Unique visitor count (resets daily)
UTM parameters Campaign tracking
Device type, browser Audience breakdown
Time on page (seconds) Engagement measurement
Scroll depth milestones Content engagement
Custom event data Your defined events

Not collected: IP addresses, raw user agents, cookies, device fingerprints, personal data.

The daily visitor hash is SHA-256 of IP + User-Agent + date. It resets every midnight UTC — impossible to track users across days.

Analytics Engine Schema

Each event writes one row to Cloudflare Analytics Engine:

Field Value
blob1 Page path
blob2 Referrer hostname (direct if none)
blob3 Country code (from CF headers)
blob4 Event name (pageview, timing, scroll_depth, custom)
blob5 Event properties (pipe-separated values)
blob6 utm_source
blob7 utm_medium
blob8 utm_campaign
blob9 Visitor hash (daily-rotating)
blob10 Site hostname (for multi-site support)
blob11 Device type (mobile/tablet/desktop)
blob12 Browser name
blob13 Operating system (Windows/macOS/iOS/Android/Linux/ChromeOS/Other)
double1 Event count (always 1)
double2 Time on page in seconds (only for timing events)
double3 Revenue/conversion value (0 unless value is sent in a /track call)

Comparison

Feature Flarelytics Google Analytics Plausible Counterscale
Privacy-first Yes No Yes Yes
No cookies Yes No Yes Yes
Self-hosted Yes No Yes Yes
Runs on CF only Yes No No Yes
Scroll depth Yes Yes No No
Custom events Yes Yes Yes Limited
Email reports Yes Yes Yes No
Data retention 90d (AE) Unlimited Unlimited 90d
Setup time 5 min 10 min 30 min 5 min
Cost Free (CF free tier) Free $20+/mo Free

Multi-Site Setup

One worker can serve multiple sites. The site hostname is derived automatically from the Origin header on each /track request.

1. Allow multiple origins

In your wrangler.toml:

[vars]
ALLOWED_ORIGINS = "https://site-a.com,https://site-b.com,https://blog.example.com"

2. Add the tracker to each site

Each site uses the same worker URL:

<!-- On site-a.com -->
<script defer data-endpoint="https://your-worker.workers.dev" src="https://your-worker.workers.dev/tracker.js"></script>

<!-- On site-b.com — same script, same worker -->
<script defer data-endpoint="https://your-worker.workers.dev" src="https://your-worker.workers.dev/tracker.js"></script>

3. Query by site

All queries accept a ?site= parameter to filter by hostname:

# Traffic for site-a.com only
curl -H "X-API-Key: your-key" "https://your-worker/query?q=daily-views&period=7d&site=site-a.com"

# Traffic for site-b.com
curl -H "X-API-Key: your-key" "https://your-worker/query?q=daily-views&period=7d&site=site-b.com"

The dashboard has a site switcher dropdown for switching between configured sites.

Troubleshooting

No data showing in dashboard

  1. Check the worker is running: curl https://your-worker.workers.dev/health
  2. Verify the tracking script is loaded: open browser DevTools → Network tab → look for tracker.js
  3. Check CORS: your site's origin must be in ALLOWED_ORIGINS in wrangler.toml
  4. Wait 30-60 seconds — Analytics Engine has a short ingestion delay

"Unauthorized" on query endpoint

The X-API-Key header must match the QUERY_API_KEY secret you set:

# Set or update the key
npx wrangler secret put QUERY_API_KEY

# Test it
curl -H "X-API-Key: your-key" "https://your-worker/query?q=daily-views&period=7d&site=yoursite.com"

"Forbidden" on /track

Your site's origin is not in ALLOWED_ORIGINS. Add it to wrangler.toml and redeploy:

[vars]
ALLOWED_ORIGINS = "https://yoursite.com,http://localhost:3000"

Bot traffic overwhelming real data

Flarelytics filters bots automatically and records them separately. Check the Bot Traffic section in the dashboard to see what's being filtered. To add custom bot patterns, modify DEFAULT_BOT_PATTERNS in packages/worker/src/index.ts.

Analytics Engine 90-day limit

Cloudflare Analytics Engine retains data for 90 days. For longer retention, set up the email reports worker to receive weekly digests, or query the API periodically and store results in KV or an external database.

Development

npm install       # Install all workspace dependencies
npm run dev       # Start worker + dashboard in dev mode
npm run build     # Build all packages
npm run test      # Run tests

License

MIT

About

Privacy-first web analytics on Cloudflare. No cookies, <1KB script, 5-minute setup.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages