web

package
v0.66.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2026 License: MIT Imports: 5 Imported by: 0

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

func CalendarDays(days int, asof time.Time) (from, to time.Time)

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

func Compute(evs []event.Event, days int, asof time.Time) Result

Compute builds the overview over the last `days` calendar days (default 30) as of `asof`.

func ComputeRange added in v0.29.0

func ComputeRange(evs []event.Event, from, to time.Time) Result

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

func ComputeWindow(evs []event.Event, window time.Duration, asof time.Time) Result

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.

type Row

type Row struct {
	Value    string `json:"value"`
	Count    int    `json:"count"` // pageviews
	Visitors int    `json:"visitors"`
}

Row is one ranked value (a page, a referrer, a source...).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL