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

installstats

package
v1.0.2 Latest Latest
Warning

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

Go to latest
Published: Oct 28, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package installstats is the opt-in, anonymous install stats sender. Nothing is sent until the owner chooses Yes; see Service.Run for the rules.

Index

Constants

View Source
const (
	// EnvTelemetry set to "off" forces install stats off. No value turns them
	// on: only the owner's choice does.
	EnvTelemetry = "HATAMOTO_TELEMETRY"
	// EnvURL overrides the endpoint, for testing.
	EnvURL = "HATAMOTO_TELEMETRY_URL"
)

Environment variables an operator can set.

View Source
const DefaultURL = "https://hatamoto.dev/p/stats/v1/ping"

DefaultURL is the public endpoint install stats are sent to.

Variables

View Source
var ErrForcedOff = errors.New("install stats are turned off by the server configuration")

ErrForcedOff means HATAMOTO_TELEMETRY=off is set, so the owner cannot change the choice.

Functions

func ForcedOff

func ForcedOff(getenv func(string) string) bool

ForcedOff reports HATAMOTO_TELEMETRY=off.

func ResolveURL

func ResolveURL(getenv func(string) string) (endpoint, warn string)

ResolveURL returns the endpoint: HATAMOTO_TELEMETRY_URL when it is valid, else DefaultURL with a warning text saying why. The URL must be a plain https scheme://host/path; plain http to a loopback host is accepted only in test builds.

Types

type Backend

type Backend interface {
	Load(ctx context.Context) (State, error)
	Enable(ctx context.Context) (State, error)
	Disable(ctx context.Context) error
	Reset(ctx context.Context) (State, error)
	Claim(ctx context.Context) (id string, ok bool, err error)
	RecordResult(ctx context.Context, status string) error
}

Backend is the stored state the Service works on; *Store implements it.

type Config

type Config struct {
	Store   Backend
	Version string                       // running version; a non-release version never sends
	Channel func(context.Context) string // update channel, "" when unknown
	Getenv  func(string) string          // nil means os.Getenv
	HTTP    HTTPDoer                     // nil means a guarded client with a 10 s timeout
	Log     *slog.Logger                 // nil means slog.Default()
}

Config wires a Service.

type Consent string

Consent is the owner's stored answer.

const (
	ConsentUnset Consent = "unset" // never asked, or not answered yet
	ConsentYes   Consent = "yes"
	ConsentNo    Consent = "no"
)

type HTTPDoer

type HTTPDoer interface {
	Do(*http.Request) (*http.Response, error)
}

HTTPDoer is the subset of *http.Client the sender needs.

type Payload

type Payload struct {
	Install string `json:"install"`
	Version string `json:"version"`
	Channel string `json:"channel"`
}

Payload is the whole request body. It has exactly these three fields; adding one changes what an owner agreed to, so it needs a new consent text.

func (Payload) Pretty

func (p Payload) Pretty() string

Pretty is the body indented for display; it decodes to the same JSON as the wire body.

type Querier

type Querier interface {
	QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
	Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
}

Querier is the part of a pgx pool the Store needs.

type Service

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

Service holds the owner's choice and sends the daily ping.

func New

func New(c Config) *Service

New returns a Service. An invalid HATAMOTO_TELEMETRY_URL is reported once and the default endpoint is used.

func (*Service) Choose

func (s *Service) Choose(ctx context.Context, yes bool) error

Choose records the owner's answer. Yes creates the install ID; No deletes it.

func (*Service) Enforce

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

Enforce applies HATAMOTO_TELEMETRY=off: consent becomes no and the ID is deleted. Lifting the variable later does not resume sending; the owner turns it on again in Settings > Updates.

func (*Service) ForcedOff

func (s *Service) ForcedOff() bool

ForcedOff reports whether HATAMOTO_TELEMETRY=off is set.

func (*Service) Reset

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

Reset replaces the install ID with a new one.

func (*Service) Run

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

Run is the daily job. It makes no request unless the variable is not off, consent is yes, the build is a release, and this process won the claim for the interval. It never retries: the claim stamps the time whatever the outcome. Neither the ID nor the URL is logged or stored in the status.

func (*Service) Unanswered

func (s *Service) Unanswered(ctx context.Context) (bool, error)

Unanswered reports whether the owner still has to answer: the variable is not off and the stored choice is unset. It reads the choice only; it builds no preview, so it is cheap enough for every page view.

func (*Service) View

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

View loads the state and builds the preview.

type State

type State struct {
	Consent    Consent
	InstallID  string
	LastSentAt *time.Time
	LastOKAt   *time.Time // last successful report; unchanged by failed attempts
	LastStatus string
}

State is the stored row. InstallID is empty unless Consent is yes.

type Store

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

Store reads and writes the single hatamoto_install_stats row. It does no caching: every call goes to the database, so a choice takes effect at once on every replica.

func NewStore

func NewStore(db Querier) *Store

NewStore returns a Store over db (a *pgxpool.Pool fits).

func (*Store) Claim

func (s *Store) Claim(ctx context.Context) (id string, ok bool, err error)

Claim takes the right to send now: it stamps last_sent_at and returns the ID, but only when consent is yes and the last claim is older than the interval. The check and the stamp are one statement, so replicas and restarts cannot both send. ok is false when nothing may be sent.

func (*Store) Disable

func (s *Store) Disable(ctx context.Context) error

Disable records consent no and deletes the ID, in one statement.

func (*Store) Enable

func (s *Store) Enable(ctx context.Context) (State, error)

Enable records consent yes. An existing ID is kept; otherwise a new random one is created. This is the only place an ID comes into being besides Reset.

func (*Store) Load

func (s *Store) Load(ctx context.Context) (State, error)

Load returns the stored state; a missing row is consent unset.

func (*Store) RecordResult

func (s *Store) RecordResult(ctx context.Context, status string) error

RecordResult stores the outcome of the last send and, on "ok", the time of the success. A failed send leaves the last success time alone. The text must not contain the install ID.

func (*Store) Reset

func (s *Store) Reset(ctx context.Context) (State, error)

Reset replaces the ID with a new random one. It does nothing unless consent is yes: an ID never exists without it.

type View

type View struct {
	State       State
	ForcedOff   bool
	Preview     Payload // the exact body; carries a placeholder ID until consent is yes
	PreviewJSON string
}

View is what the admin shows.

Jump to

Keyboard shortcuts

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