Skitflow

Web Analytics

Privacy-first pageview tracking and Web Vitals for your domains, stored in ClickHouse and surfaced in the Skitflow dashboard.

Web Analytics requires the Expert or Enterprise plan on Skitflow Cloud, or the observability license feature on self-hosted installations. It shares the same ClickHouse cluster and retention settings as Observability.

Web Analytics collects pageviews and Web Vitals for every domain you enable it on. Data is stored in your ClickHouse instance — it never leaves your infrastructure on self-hosted, and stays in the Skitflow Cloud control plane on cloud deployments.

Enabling analytics for a domain

  1. Open Web Analytics in the dashboard sidebar and pick the application or Compose stack from the selector.
  2. The Domains card lists that resource's domains, each with its own enable toggle.
  3. Flip the toggle for a domain. Skitflow creates a siteId for it and re-renders the Traefik configuration to attach the analytics inject middleware.

Disabling a domain triggers the same Traefik re-render and detaches the middleware. The siteId is preserved, so re-enabling the same domain keeps any existing snippet configuration working.

Auto-inject only runs when SKITFLOW_WEB_ANALYTICS_AUTOINJECT is set to true, and it never applies to compose domains or Kubernetes apps — the config re-render is skipped for those entirely. For anything outside plain Traefik-managed applications with auto-inject on, paste the snippet manually (see below).

Manual beacon snippet

Once a domain is enabled, the Domains card shows a copyable snippet built from its siteId:

<script defer src="<skitflow-url>/va.js" data-site="<siteId>"></script>

Paste this into the page <head> of the tracked site. This is required — not optional — for Compose domains and Kubernetes applications, since auto-inject never runs for them; for plain Traefik-managed applications it's a fallback for when the auto-inject flag is off.

Required permission: observability.manage

API endpoints:

POST /api/trpc/webAnalytics.enable   { "domainId": "<id>" }
POST /api/trpc/webAnalytics.disable  { "domainId": "<id>" }
GET  /api/trpc/webAnalytics.status?input={"domainId":"<id>"}

status returns { enabled: boolean, siteId: string | null }.

Retention

Web Analytics uses the same retention period configured in Observability Settings (default: 30 days). The from parameter on every query is automatically clamped to now − retentionDays, so requests for data older than the retention window silently return an empty result rather than an error.

Raw pageview and Web Vitals events are stored in ClickHouse with a 90-day TTL. Hourly pre-aggregated rollups serve the common app-wide queries efficiently within the retention window.

Dashboard data

getAnalytics fetches all eleven dashboard cards in a single call:

CardDescription
viewsOverTimeHourly pageview counts across the time range
topPathsTop 500 paths by view count
topReferrersTop 50 referring hosts (direct / empty referrer excluded)
countriesTop 50 countries by pageview count
regionsTop 50 regions by pageview count
citiesTop 50 cities by pageview count
devicesTop 50 device types (desktop, mobile, tablet, …)
browsersTop 50 browsers
operatingSystemsTop 50 operating systems
vitalsp75 Web Vitals score per metric across the period
vitalsRatingsGood / needs-improvement / poor counts per metric

API endpoint: GET /api/trpc/webAnalytics.getAnalytics

Required input: { appId, from, to } — ISO 8601 datetime strings for from/to. Optional: serviceName (see Per-service scope).

Required permission: observability.read

Metrics summary

getMetrics returns the pageview top-paths list and the Web Vitals p75 summary in one call — a lighter alternative to getAnalytics when only those two cards are needed.

API endpoint: GET /api/trpc/webAnalytics.getMetrics

Views timeseries

getViewsTimeseries returns the hourly views-over-time series for a single service. The per-service "Compare" view fans this out — one query per compose service — and overlays the series as colored lines.

API endpoint: GET /api/trpc/webAnalytics.getViewsTimeseries

Web Vitals timeseries

getVitalsTimeseries returns time-bucketed p75 Web Vitals data. The bucket width is configurable in whole hours (1–24); it defaults to 1 h, which is the native rollup grain.

InputTypeDescription
appIdstringApplication or Compose stack ID
fromdatetimeISO 8601 range start
todatetimeISO 8601 range end
bucketHoursint (1–24)Rollup bucket width (default: 1)
serviceNamestringOptional per-service filter

API endpoint: GET /api/trpc/webAnalytics.getVitalsTimeseries

Web Vitals scores are rated good, needs-improvement, or poor using the standard Core Web Vitals thresholds.

Per-service scope

All read endpoints accept an optional serviceName parameter that narrows the results to a single compose service. A compose service can own multiple domains, so the filter resolves to the set of siteId values for that service's analytics-enabled domains.

  • Omitting serviceName returns app-wide data merged across all domains, served from the pre-aggregated 1 h rollup.
  • Setting serviceName to a service with no analytics-enabled domains returns empty results rather than falling back to app-wide data.

Per-service queries hit the raw pageview_events / web_vitals_events tables (the rollups drop site_id), but they're still clamped to the same retention window as every other query. The 90-day raw-table TTL is only the physical ceiling — on self-hosted it shrinks to match a configured retention below 90 days.

Data collected

Each pageview event records:

FieldNotes
pathURL path (no query string)
referrer_hostReferring hostname; empty for direct visits
country, region, cityGeoIP-resolved from the client IP
device_typeDerived from the user-agent
browserDerived from the user-agent
osDerived from the user-agent
client_ip_hashSHA-256 of the client IP, truncated to 16 hex chars — only the hash is stored, never the raw IP

Each Web Vitals event records the metric name, value, rating, path, and device type. The raw client IP is never persisted.

Privacy

Client IP addresses are hashed (SHA-256, truncated to 16 hex chars) before storage — only the hash is written to ClickHouse, never the raw IP. No cookies or fingerprinting are used. On self-hosted installations all data stays on your own infrastructure.

On this page