domain/analytics/goatcounter/count.go (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | 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
}
|