package goatcounter
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"time"
"alin.ovh/x/log"
"github.com/Southclaws/fault"
"github.com/Southclaws/fault/fmsg"
"alin.ovh/homestead/shared/config"
)
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"`
Title string `json:"title"`
UserAgent string `json:"user_agent"`
}
type countBody struct {
NoSessions bool `json:"no_sessions"`
Hits []hit `json:"hits"`
}
const timeout = 5 * time.Second
func New(options *Options) *Goatcounter {
baseURL := options.URL
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, title string) {
err := gc.count(r, title)
if err != nil {
gc.log.Warn("could not log page view", "error", err)
}
}
func (gc *Goatcounter) count(userReq *http.Request, title string) error {
body, err := json.Marshal(&countBody{
NoSessions: true,
Hits: []hit{
{
IP: userReq.RemoteAddr,
Path: userReq.URL.Path,
Query: userReq.URL.RawQuery,
Title: title,
Referrer: userReq.Header.Get("Referer"),
UserAgent: userReq.Header.Get("User-Agent"),
},
},
})
if err != nil {
return fault.Wrap(err, fmsg.With("could not marshal JSON"))
}
go func(body []byte) {
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 {
gc.log.Warn("could not create HTTP request", "error", err)
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+gc.token)
res, err := gc.client.Do(req)
if err != nil {
gc.log.Warn("could not perform HTTP request", "error", err)
return
}
clear(body)
if res.Body != nil {
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)
}
}(body)
return nil
}
domain/analytics/goatcounter/count.go (view raw)