all repos — homestead @ 522324c7b7491a359d7a37931bfc2da402d014b0

Code for my website

move goatcounter integration to server-side

Alan Pearce
commit

522324c7b7491a359d7a37931bfc2da402d014b0

parent

6db99a292ad6a3ba7c5fedce1a4337d3ceac78af

1 file changed, 124 insertions(+), 0 deletions(-)

changed files
A internal/stats/goatcounter/count.go
@@ -0,0 +1,124 @@
+package goatcounter + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "strings" + "time" + + "gitlab.com/tozd/go/errors" + "go.alanpearce.eu/homestead/internal/config" + "go.alanpearce.eu/x/log" +) + +const timeout = 1 * time.Second + +type Options struct { + URL *config.URL + Logger *log.Logger + Token string +} + +type URLs struct { + count string +} + +type Goatcounter struct { + log *log.Logger + token string + client *http.Client + urls URLs +} + +type hit struct { + IP string `json:"ip"` + Path string `json:"path"` + Query string `json:"query"` + Referrer string `json:"ref"` + UserAgent string `json:"user_agent"` +} + +type countBody struct { + NoSessions bool `json:"no_sessions"` + Hits []hit `json:"hits"` +} + +func New(options *Options) *Goatcounter { + baseURL := options.URL + if strings.HasSuffix(baseURL.Path, "/count") { + baseURL.Path = "/api/v0/" + } + + return &Goatcounter{ + log: options.Logger, + token: options.Token, + client: &http.Client{ + Timeout: 5 * time.Second, + }, + urls: URLs{ + count: baseURL.JoinPath("count").String(), + }, + } +} + +func (gc *Goatcounter) Count(r *http.Request) { + err := gc.count(r) + if err != nil { + gc.log.Warn("could not log page view", "error", err) + } +} + +func (gc *Goatcounter) count(userReq *http.Request) error { + body, err := json.Marshal(&countBody{ + NoSessions: true, + Hits: []hit{ + { + IP: userReq.RemoteAddr, + Path: userReq.URL.Path, + Query: userReq.URL.RawQuery, + Referrer: userReq.Header.Get("Referer"), + UserAgent: userReq.Header.Get("User-Agent"), + }, + }, + }) + if err != nil { + return errors.WithMessage(err, "could not marshal JSON") + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + gc.urls.count, + bytes.NewBuffer(body), + ) + if err != nil { + return errors.WithMessage(err, "could not create HTTP request") + } + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+gc.token) + + go func(req *http.Request) { + res, err := gc.client.Do(req) + if err != nil { + gc.log.Warn("could not perform HTTP request", "error", err) + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + gc.log.Warn("could not read error body", "error", err) + } + if res.StatusCode != http.StatusAccepted { + gc.log.Warn("failed to log pageview", "status", res.StatusCode, "body", body) + } + + }(req) + + return nil +}