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
- Open Web Analytics in the dashboard sidebar and pick the application or Compose stack from the selector.
- The Domains card lists that resource's domains, each with its own enable toggle.
- Flip the toggle for a domain. Skitflow creates a
siteIdfor 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:
| Card | Description |
|---|---|
viewsOverTime | Hourly pageview counts across the time range |
topPaths | Top 500 paths by view count |
topReferrers | Top 50 referring hosts (direct / empty referrer excluded) |
countries | Top 50 countries by pageview count |
regions | Top 50 regions by pageview count |
cities | Top 50 cities by pageview count |
devices | Top 50 device types (desktop, mobile, tablet, …) |
browsers | Top 50 browsers |
operatingSystems | Top 50 operating systems |
vitals | p75 Web Vitals score per metric across the period |
vitalsRatings | Good / 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.
| Input | Type | Description |
|---|---|---|
appId | string | Application or Compose stack ID |
from | datetime | ISO 8601 range start |
to | datetime | ISO 8601 range end |
bucketHours | int (1–24) | Rollup bucket width (default: 1) |
serviceName | string | Optional 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
serviceNamereturns app-wide data merged across all domains, served from the pre-aggregated 1 h rollup. - Setting
serviceNameto 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:
| Field | Notes |
|---|---|
path | URL path (no query string) |
referrer_host | Referring hostname; empty for direct visits |
country, region, city | GeoIP-resolved from the client IP |
device_type | Derived from the user-agent |
browser | Derived from the user-agent |
os | Derived from the user-agent |
client_ip_hash | SHA-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.
Related
Observability
Runtime logs, distributed traces and metrics for your apps, collected by telemetry agents and stored in ClickHouse.
AI Assistant
Use Skitflow's AI Assistant to generate Compose files, analyze deployments, harden configuration, answer product questions, and propose confirmed operations through AI Gateway.