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
- Variables
- func ForcedOff(getenv func(string) string) bool
- func ResolveURL(getenv func(string) string) (endpoint, warn string)
- type Backend
- type Config
- type Consent
- type HTTPDoer
- type Payload
- type Querier
- type Service
- func (s *Service) Choose(ctx context.Context, yes bool) error
- func (s *Service) Enforce(ctx context.Context) error
- func (s *Service) ForcedOff() bool
- func (s *Service) Reset(ctx context.Context) error
- func (s *Service) Run(ctx context.Context) error
- func (s *Service) Unanswered(ctx context.Context) (bool, error)
- func (s *Service) View(ctx context.Context) (View, error)
- type State
- type Store
- func (s *Store) Claim(ctx context.Context) (id string, ok bool, err error)
- func (s *Store) Disable(ctx context.Context) error
- func (s *Store) Enable(ctx context.Context) (State, error)
- func (s *Store) Load(ctx context.Context) (State, error)
- func (s *Store) RecordResult(ctx context.Context, status string) error
- func (s *Store) Reset(ctx context.Context) (State, error)
- type View
Constants ¶
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.
const DefaultURL = "https://hatamoto.dev/p/stats/v1/ping"
DefaultURL is the public endpoint install stats are sent to.
Variables ¶
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 ResolveURL ¶
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 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.
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 ¶
New returns a Service. An invalid HATAMOTO_TELEMETRY_URL is reported once and the default endpoint is used.
func (*Service) Choose ¶
Choose records the owner's answer. Yes creates the install ID; No deletes it.
func (*Service) Enforce ¶
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) Run ¶
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 ¶
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.
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 (*Store) Claim ¶
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) Enable ¶
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) RecordResult ¶
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.