Help us shape the future of Go! Take the 2026 Go Developer Survey

stats

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 32 Imported by: 0

README

Stats

Page view statistics without cookies: views and visitors per day and per page, referring sites, devices and languages, and "most read" lists for templates.

It is a site feature: one instance counts the whole public site. It sets no cookies and stores no IP addresses or visitor IDs, only totals per day.

Add it to the site

app.Use(stats.Module())

The instance gets the key stats and the title Statistics, translated in the admin; its dashboard is at /admin/stats (/admin is the default admin path). Named chooses another key and title; the template functions then take that key:

app.Use(stats.Module(stats.Named("visits", "Visits")))

What is counted

Every public HTML page that answers a GET request with 200 counts as a view. Per day the module keeps the views and visitors of the site and of each page, with the page's title, and of:

  • referring sites, by host without www.; links within the site do not count, also behind a proxy listed in Config.TrustedProxies, which names the site's host;
  • device classes: desktop, mobile or tablet, from the user agent;
  • languages: the first segment of the path, such as en.

A day is a date in the site's timezone (Site settings › Timezone), also on a day its clocks skip midnight. The days of the dashboard, of popular and of Keep statistics for (days) count back from today's date there.

Not counted are the paths WebThings serves itself: the admin, visitor actions (/_/), /media/, /assets/, /theme/, /sitemap.xml, /robots.txt, /healthz, /readyz and /metrics. Nor are other paths with a file extension; HEAD requests, prefetches and htmx fragments (boosted links do count); bots, link previews, monitors and scripts, recognised by their user agent, and requests without one; and editors who are signed in to the admin.

Pages that ask not to be kept do not count either: answers with X-Robots-Tag: noindex or none, also for a single crawler, with Cache-Control: private, or with <meta name="robots" content="noindex"> in the page. Search results and registrations looked up by their code are such pages, so their addresses never show up in the dashboard or in "most read" lists. A bare Cache-Control: no-store still counts.

Visitors and privacy

A visitor is recognised by a hash of a random salt, the IP address and the user agent. The salt lives in memory only and changes every day, in the site's timezone, so a visitor cannot be followed from one day to the next. Neither the address nor the hash is stored. Visitors over a period are therefore the sum of each day's visitors.

Each process has its own salt: after a restart, or on another server behind a load balancer, the same visitor counts again that day. Behind a reverse proxy, list it in Config.TrustedProxies; otherwise every visitor has the proxy's address.

Show it on any page

pageViews is how often a path was viewed in the kept statistics; an empty path is the current page:

<p>Read {{pageViews "stats" ""}} times</p>

popular lists the most viewed pages in the current language, without the home page. Its arguments are the number of pages, where 0 or less lists all, and the number of days, today included. An optional last argument limits the list to the pages below a module instance's page ("news") or below a path without the language prefix ("/blog"), without that page itself:

<h2>Most read</h2>
{{range popular "stats" 5 30 "news"}}
    <a href="{{.Path}}">{{.Title}}</a>
{{end}}

Title is the page's HTML <title> without the site name the head region adds: Hello · My site is kept as Hello. The separators ·, |, -, –, —, :: and / are recognised. A page whose title changed shows the title of the last day it was viewed. An instance that is not placed on a page has no popular pages.

In the admin

The dashboard shows the last 7, 30 or 90 days: the totals, a chart of the page views per day, the top 10 pages, the top 10 referring sites, the devices and the languages. It writes the views counted in memory first, so its numbers are up to date.

Editors who may edit the instance set Keep statistics for (days). Older days are deleted right away and then once a day; 0 keeps everything.

How it works

The module wraps the public site and counts a view after the page was sent, in memory. It runs before the site's output cache, so pages the cache answers without rendering them count too (see Modules and the output cache), and it reads the head of a compressed page unpacked. Views are written to the database at most 30 seconds later, so a page never waits for it; closing the app writes what is left. pageViews includes the views that are not written yet.

From Go

stats.Of(app, "stats") returns the instance once the app has started:

counter := stats.Of(app, "stats")

report, err := counter.Report(ctx, 30)

Report sums up the last days as the dashboard shows them. Popular, PopularIn and Views are what the template functions use. Flush writes the views counted in memory, for example in tests, and SetRetention sets how many days are kept.

  • Publishing posts make good "most read" lists.
  • Redirects lists the paths visitors asked for that do not exist.

Documentation

Overview

Package stats counts the public site's page views without cookies and without storing anything about visitors. Add an instance with:

app.Use(stats.Module())

The instance has the key "stats", used in the template functions below; stats.Module(stats.Named("visits", "Visits")) chooses another key and title.

Every public HTML page that answers 200 to a GET is counted: views and visitors per day and per page, referrer hosts, device classes (desktop, mobile, tablet) and languages (the first path segment). Not counted are the paths WebThings serves itself (webthings.App.CorePaths: the admin, visitor actions (/_/), media, assets, theme files, sitemap.xml, robots.txt, /healthz, /readyz and /metrics), files with an extension, HEAD requests, prefetches, htmx fragments, bots and editors who are signed in to the admin (frontend.Editor). Nor are pages that ask not to be kept, so their addresses never show up in the admin or in "most read" lists: answers with "X-Robots-Tag: noindex" (or none), "Cache-Control: private" or <meta name="robots" content="noindex">, such as search results and registrations looked up by their code. A bare "Cache-Control: no-store" still counts, since forms, polls and comments send it to stay fresh.

A visitor is recognised by a hash of a random salt, the IP address (frontend.ClientIP, so list a reverse proxy in Config.TrustedProxies) and the user agent. The salt lives in memory only and changes every day, so visitors cannot be followed from day to day and nothing in the database leads back to a person. After a restart the same day's visitors count once more.

Views are counted in memory and written at most 30 seconds later, so a page never waits for the database. Closing the app writes what is left.

The admin shows the last 7, 30 or 90 days with a chart per day, the top pages, referrers, devices and languages, and a retention setting that deletes older days. The module has no page of its own.

Template functions:

{{pageViews "stats" .URL}}            views of a path, e.g. "read {{pageViews "stats" .URL}} times";
                                      an empty path is the current page
{{range popular "stats" 5 30}}        the 5 most viewed pages in the current language
    <a href="{{.Path}}">{{.Title}}</a>  of the last 30 days, without the home page;
{{end}}                               each has Path, Title, Views and Visitors;
                                      a limit of 0 lists all pages
{{range popular "stats" 5 30 "news"}} the same below the page of a module instance,
{{range popular "stats" 5 30 "/blog"}} or below a path (without the language prefix),
                                      without that page itself: "most read" posts

Title is the page's HTML <title> when it was last viewed, without the site name the layout adds before or after it ("Hello · My site" is stored as "Hello"; the separators ·, |, -, –, —, :: and / are recognised).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Module

func Module(opts ...Option) webthings.Module

Module adds the Stats instance, with the key "stats" and the title "Statistics" (translated in the admin) unless Named says otherwise. A site needs one: it counts every public page.

Types

type Bar

type Bar struct {
	// Date is the day in the admin language, such as "5 okt 2026".
	Date     string
	Views    int
	Visitors int
	Height   int
}

Bar is one day of the chart. Height is a percentage of the busiest day.

type Count

type Count struct {
	Name     string `db:"name"`
	Views    int    `db:"views"`
	Visitors int    `db:"visitors"`
}

Count is the number of views by one referrer host, device class (desktop, mobile or tablet) or language.

type DashboardView

type DashboardView struct {
	Title    string
	Days     int
	Ranges   []RangeLink
	Views    int
	Visitors int
	// From and To are the first and last day of the chart, in the admin
	// language.
	From      string
	To        string
	Bars      []Bar
	Pages     []Row
	Referrers []Row
	Devices   []Row
	Locales   []Row
	// Settings is the retention form, for users who may edit.
	Settings       *form.RenderModel
	SettingsErrors form.FieldErrors
}

DashboardView is the data of the admin dashboard.

type Day

type Day struct {
	// Date is YYYY-MM-DD.
	Date     string `db:"day"`
	Views    int    `db:"views"`
	Visitors int    `db:"visitors"`
}

Day is one day of a report.

type Option

type Option func(*options)

Option configures the instance.

func Named

func Named(key, title string) Option

Named gives the instance its own key, used in URLs of the admin and in the template functions, and title, shown in the admin, instead of "stats" and "Statistics".

type Page

type Page struct {
	// Path is the page's address, for example /en/news/hello.
	Path string `db:"path"`
	// Title is the page's HTML title when it was last viewed, without the
	// site name.
	Title    string `db:"title"`
	Views    int    `db:"views"`
	Visitors int    `db:"visitors"`
}

Page is a path with its number of views.

type RangeLink struct {
	Days   int
	URL    string
	Active bool
}

RangeLink switches the dashboard's period.

type Report

type Report struct {
	Days int
	// Views and Visitors are the totals. A visitor is recognised for one day
	// only, so Visitors adds up the visitors of each day.
	Views     int
	Visitors  int
	Daily     []Day
	Pages     []Page
	Referrers []Count
	Devices   []Count
	Locales   []Count
}

Report is the statistics of the last Days days, including today.

type Row

type Row struct {
	Name     string
	Detail   string
	URL      string
	Views    int
	Visitors int
	Share    int
}

Row is a line of a top list. Share is a percentage of the list's first row.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service is one Stats instance, for the website's own code.

func Of

func Of(app *webthings.App, key string) *Service

Of returns the Stats instance with key from a started app, or nil.

func (*Service) Flush

func (s *Service) Flush(ctx context.Context) error

Flush writes the page views counted in memory. It runs by itself every 30 seconds after a page view and when the app closes; call it to read up-to-date numbers, for example in tests.

func (*Service) Popular

func (s *Service) Popular(ctx context.Context, locale string, limit, days int) ([]Page, error)

Popular returns the limit most viewed pages in locale of the last days days, without the home page, for "most read" lists. A limit of 0 or less returns all of them.

func (*Service) PopularIn

func (s *Service) PopularIn(ctx context.Context, locale, within string, limit, days int) ([]Page, error)

PopularIn is Popular for the pages below within, without that page itself, for example the most read posts of a blog. within is a path without the language prefix, such as "/blog", or the key of a module instance, whose page in locale is used; an instance that is not on a page has no popular pages. A limit of 0 or less returns all of them.

func (*Service) Prune

func (s *Service) Prune(ctx context.Context) error

Prune deletes the days that are older than the retention. Flush calls it once a day.

func (*Service) Report

func (s *Service) Report(ctx context.Context, days int) (Report, error)

Report sums up the last days days (1 to 366), today included. It reads what was flushed; call Flush first for up-to-the-second numbers.

func (*Service) Retention

func (s *Service) Retention(ctx context.Context) (int, error)

Retention is the number of days statistics are kept; 0 keeps them all.

func (*Service) SetRetention

func (s *Service) SetRetention(ctx context.Context, days int) error

SetRetention keeps statistics for days days (0 keeps them all) and deletes older ones right away.

func (*Service) Views

func (s *Service) Views(ctx context.Context, path string) (int, error)

Views is the number of times path was viewed in the kept statistics, including views that are not flushed yet.

Jump to

Keyboard shortcuts

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