Documentation
¶
Overview ¶
Package web composes the one-glance web-analytics view — live visitors, top pages, referrers, UTM sources, device split — from $pageview events. This is the Plausible-shaped report indie devs otherwise run a SECOND tool for; here it's the same engine, same events, one binary. Deterministic like every other report.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CalendarDays ¶ added in v0.45.0
CalendarDays is THE definition of "the last N days" for this product: N complete calendar day buckets ending today, i.e. [midnight (N-1) days ago, now).
It exists because there were TWO definitions, and they disagreed on live data. The dashboard windowed by calendar days (via ComputeRange) while Compute windowed by a rolling N×24h, which reaches back a further (24h - time-of-day) — about ten hours at midday. Measured on the demo at the same instant, same days=30, same event: /v1/web reported 1,779 visitors and 2,371 pageviews while /v1/rows reported 1,753 and 2,336. Same question, two answers.
That is not a rounding difference, it is the product's core promise failing: "ask == dashboard == MCP, all from one report engine" is on the footer, and web_overview (MCP), /v1/web (HTTP) and the public share page all used the rolling form while the dashboard beside them used the calendar form. It is the same defect as the sampler leak that once had the verdict card saying 4% retention while the ask bar said 6% three inches above it.
Calendar is the correct side to converge on: parseTrendWindow already documents why (a rolling window clips the oldest bucket mid-day and renders a phantom leading partial day on every chart), and every trend, funnel, retention and rows query in the product already uses it. One definition now, so a surface can only diverge on purpose.
Types ¶
type Recorded ¶ added in v0.29.0
type Recorded struct {
TopPages int `json:"top_pages"`
Referrers int `json:"referrers"`
UTMSources int `json:"utm_sources"`
DeviceSplit int `json:"device_split"`
Browsers int `json:"browsers"`
OSes int `json:"oses"`
Countries int `json:"countries"`
UTMMediums int `json:"utm_mediums"`
UTMCampaigns int `json:"utm_campaigns"`
EntryPages int `json:"entry_pages"`
ExitPages int `json:"exit_pages"`
TopTitles int `json:"top_titles"`
AIReferrers int `json:"ai_referrers"`
}
Recorded is the untruncated total behind each ranked dimension: how many pageviews (for per-pageview dimensions) or first-touch visitors (for the per-visitor ones) actually carried a value, before rank() cut the list down to what fits on screen.
type Result ¶
type Result struct {
PeriodDays int `json:"period_days"`
Visitors int `json:"visitors"` // unique visitors in period
Pageviews int `json:"pageviews"` // total $pageview in period
LiveNow int `json:"live_now"` // unique visitors in the last 5 minutes
TopPages []Row `json:"top_pages"`
Referrers []Row `json:"referrers"` // grouped by host, "" → "direct"
UTMSources []Row `json:"utm_sources"` // only when utm_source is present
DeviceSplit []Row `json:"device_split"` // mobile / desktop
// the dimensions people open first — browsers/os come from the ingest-time UA
// parse, countries from ingest-time geo (when enabled), entry pages from the
// SDK's session_id, titles from the SDK's title prop
Browsers []Row `json:"browsers"`
OSes []Row `json:"oses"`
Countries []Row `json:"countries"`
UTMMediums []Row `json:"utm_mediums"`
UTMCampaigns []Row `json:"utm_campaigns"`
EntryPages []Row `json:"entry_pages"`
ExitPages []Row `json:"exit_pages"` // last pageview of each session — where visits END
TopTitles []Row `json:"top_titles"`
Hours [24]int `json:"hours"` // pageviews by UTC hour of day — the activity rhythm
// engagement — from $engagement events (SDK measures visible+focused time).
// Omitted (zero) when the SDK predates engagement tracking.
HasEngagement bool `json:"has_engagement"`
AvgEngagedSecs int `json:"avg_engaged_secs"` // mean engaged time per engaged visitor
BounceRatePct int `json:"bounce_rate_pct"` // 1 pageview AND <10s engaged
// the AI channel — humans arriving FROM AI assistants (chatgpt/claude/perplexity...).
// distinct from AI crawlers, which the bot filter drops before storage.
AIVisitors int `json:"ai_visitors"`
AIReferrers []Row `json:"ai_referrers"`
// Every ranked list above is TRUNCATED to its top N. A reader that sums the rows it can
// see and calls that the total gets a denominator that only covers the visible head: a
// site with 40 countries, 20 US visitors out of 100, rendered "US 20 · 52%" because the
// ten visible rows summed to 38. The visible shares then also add to exactly 100%,
// asserting there is no tail at all. These are the pre-truncation recorded totals — the
// only honest denominator for "share of recorded" — so no surface has to re-derive one
// from rows it was already handed in truncated form.
Recorded Recorded `json:"recorded"`
}
Result is the web overview for a period.
func Compute ¶
Compute builds the overview over the last `days` calendar days (default 30) as of `asof`.
func ComputeRange ¶ added in v0.29.0
ComputeRange is ComputeWindow over an EXPLICIT [from, to) — the form the dashboard needs.
ComputeWindow derives its bounds from a duration and an as-of, which produces a ROLLING window: "last 10 days" means now−240h. The trends path next to it aligns the same preset to calendar days (day0−9d .. now) precisely so the tile, /v1/trends and the MCP tool agree. Two tiles sat side by side on the same row, both labelled "· 10d", measuring windows that differ by up to a day at each edge — and on a young instance that is the difference between a prior window with data in it and one without, so one tile showed "71x vs prior" and the one beside it showed no delta at all. Taking the bounds as arguments is what makes ONE window definition possible for the whole row.
func ComputeWindow ¶ added in v0.15.0
ComputeWindow is Compute over an arbitrary window rather than a whole number of days, so the dashboard's sub-day presets (6h, 12h) can report the SAME window in the tiles as the chart. Without it a 6h range moved the chart only and left every tile on 24h, which is a range control that lies about what it changed.