diff --git a/CHANGELOG.md b/CHANGELOG.md index ede5cb8..75370b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 2.0.0 + +* added optional client hint headers +* added structured logging using `log/slog` +* removed DNT +* changed package structure +* fixed options to extend sessions +* upgraded Go version to 1.21 +* updated dependencies + ## 1.9.0 * added configuration options for request timeout and retries diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..57893e0 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +.PHONY: deps test + +deps: + go get -u -t ./... + +test: + go test -cover -race github.com/pirsch-analytics/pirsch-go-sdk/v2/pkg diff --git a/go.mod b/go.mod index 60ad1d4..01bf94a 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ -module github.com/pirsch-analytics/pirsch-go-sdk +module github.com/pirsch-analytics/pirsch-go-sdk/v2 -go 1.20 +go 1.21 require ( github.com/emvi/null v1.3.1 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.4 ) require ( diff --git a/go.sum b/go.sum index 2941490..d7b447b 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/client.go b/pkg/client.go similarity index 84% rename from client.go rename to pkg/client.go index f787f93..07ecc13 100644 --- a/client.go +++ b/pkg/client.go @@ -1,4 +1,4 @@ -package pirsch +package pkg import ( "bytes" @@ -6,9 +6,10 @@ import ( "errors" "fmt" "io" - "log" + "log/slog" "net/http" "net/url" + "os" "strconv" "sync" "time" @@ -67,7 +68,7 @@ var referrerQueryParams = []string{ // Client is used to access the Pirsch API. type Client struct { baseURL string - logger *log.Logger + logger *slog.Logger clientID string clientSecret string accessToken string @@ -90,19 +91,25 @@ type ClientConfig struct { RequestRetries int // Logger is an optional logger for debugging. - Logger *log.Logger -} - -// HitOptions optional parameters to send with the hit request. -type HitOptions struct { - URL string - IP string - UserAgent string - AcceptLanguage string - Title string - Referrer string - ScreenWidth int - ScreenHeight int + Logger slog.Handler +} + +// PageViewOptions optional parameters to send with the hit request. +type PageViewOptions struct { + URL string + IP string + UserAgent string + AcceptLanguage string + SecCHUA string + SecCHUAMobile string + SecCHUAPlatform string + SecCHUAPlatformVersion string + SecCHWidth string + SecCHViewportWidth string + Title string + Referrer string + ScreenWidth int + ScreenHeight int } // NewClient creates a new client for given client ID, client secret, hostname, and optional configuration. @@ -128,9 +135,13 @@ func NewClient(clientID, clientSecret string, config *ClientConfig) *Client { config.RequestRetries = defaultRequestRetries } + if config.Logger == nil { + config.Logger = slog.NewTextHandler(os.Stderr, nil) + } + c := &Client{ baseURL: config.BaseURL, - logger: config.Logger, + logger: slog.New(config.Logger), clientID: clientID, clientSecret: clientSecret, timeout: config.Timeout, @@ -145,67 +156,54 @@ func NewClient(clientID, clientSecret string, config *ClientConfig) *Client { return c } -// Hit sends a page hit to Pirsch for given http.Request. -func (client *Client) Hit(r *http.Request) error { - return client.HitWithOptions(r, nil) -} - -// HitWithOptions sends a page hit to Pirsch for given http.Request and options. -func (client *Client) HitWithOptions(r *http.Request, options *HitOptions) error { - if r.Header.Get("DNT") == "1" { - return nil - } - +// PageView sends a page hit to Pirsch for given http.Request and options. +func (client *Client) PageView(r *http.Request, options *PageViewOptions) error { if options == nil { - options = new(HitOptions) + options = new(PageViewOptions) } - hit := client.getHit(r, options) + hit := client.getPageViewData(r, options) return client.performPost(client.baseURL+hitEndpoint, &hit, client.requestRetries) } -// Event sends an event to Pirsch for given http.Request. -func (client *Client) Event(name string, durationSeconds int, meta map[string]string, r *http.Request) error { - return client.EventWithOptions(name, durationSeconds, meta, r, nil) -} - -// EventWithOptions sends an event to Pirsch for given http.Request and options. -func (client *Client) EventWithOptions(name string, durationSeconds int, meta map[string]string, r *http.Request, options *HitOptions) error { +// Event sends an event to Pirsch for given http.Request and options. +func (client *Client) Event(name string, durationSeconds int, meta map[string]string, r *http.Request, options *PageViewOptions) error { if r.Header.Get("DNT") == "1" { return nil } if options == nil { - options = new(HitOptions) + options = new(PageViewOptions) } return client.performPost(client.baseURL+eventEndpoint, &Event{ Name: name, DurationSeconds: durationSeconds, Metadata: meta, - Hit: client.getHit(r, options), + PageView: client.getPageViewData(r, options), }, client.requestRetries) } -// Session keeps a session alive for the given http.Request. -func (client *Client) Session(r *http.Request) error { - return client.HitWithOptions(r, nil) -} - -// SessionWithOptions keeps a session alive for the given http.Request and options. -func (client *Client) SessionWithOptions(r *http.Request, options *HitOptions) error { +// Session keeps a session alive for the given http.Request and options. +func (client *Client) Session(r *http.Request, options *PageViewOptions) error { if r.Header.Get("DNT") == "1" { return nil } if options == nil { - options = new(HitOptions) - } - - return client.performPost(client.baseURL+sessionEndpoint, &Hit{ - URL: r.URL.String(), - IP: r.RemoteAddr, - UserAgent: r.Header.Get("User-Agent"), + options = new(PageViewOptions) + } + + return client.performPost(client.baseURL+sessionEndpoint, &PageView{ + URL: r.URL.String(), + IP: client.selectField(options.IP, r.RemoteAddr), + UserAgent: client.selectField(options.UserAgent, r.Header.Get("User-Agent")), + SecCHUA: client.selectField(options.SecCHUA, r.Header.Get("Sec-CH-UA")), + SecCHUAMobile: client.selectField(options.SecCHUAMobile, r.Header.Get("Sec-CH-UA-Mobile")), + SecCHUAPlatform: client.selectField(options.SecCHUAPlatform, r.Header.Get("Sec-CH-UA-Platform")), + SecCHUAPlatformVersion: client.selectField(options.SecCHUAPlatformVersion, r.Header.Get("Sec-CH-UA-Platform-Version")), + SecCHWidth: client.selectField(options.SecCHWidth, r.Header.Get("Sec-CH-Width")), + SecCHViewportWidth: client.selectField(options.SecCHViewportWidth, r.Header.Get("Sec-CH-Viewport-Width")), }, client.requestRetries) } @@ -554,16 +552,22 @@ func (client *Client) Keywords(filter *Filter) ([]Keyword, error) { return stats, nil } -func (client *Client) getHit(r *http.Request, options *HitOptions) Hit { - return Hit{ - URL: client.selectField(options.URL, r.URL.String()), - IP: client.selectField(options.IP, r.RemoteAddr), - UserAgent: client.selectField(options.UserAgent, r.Header.Get("User-Agent")), - AcceptLanguage: client.selectField(options.AcceptLanguage, r.Header.Get("Accept-Language")), - Title: options.Title, - Referrer: client.selectField(options.Referrer, client.getReferrerFromHeaderOrQuery(r)), - ScreenWidth: options.ScreenWidth, - ScreenHeight: options.ScreenHeight, +func (client *Client) getPageViewData(r *http.Request, options *PageViewOptions) PageView { + return PageView{ + URL: client.selectField(options.URL, r.URL.String()), + IP: client.selectField(options.IP, r.RemoteAddr), + UserAgent: client.selectField(options.UserAgent, r.Header.Get("User-Agent")), + AcceptLanguage: client.selectField(options.AcceptLanguage, r.Header.Get("Accept-Language")), + SecCHUA: client.selectField(options.SecCHUA, r.Header.Get("Sec-CH-UA")), + SecCHUAMobile: client.selectField(options.SecCHUAMobile, r.Header.Get("Sec-CH-UA-Mobile")), + SecCHUAPlatform: client.selectField(options.SecCHUAPlatform, r.Header.Get("Sec-CH-UA-Platform")), + SecCHUAPlatformVersion: client.selectField(options.SecCHUAPlatformVersion, r.Header.Get("Sec-CH-UA-Platform-Version")), + SecCHWidth: client.selectField(options.SecCHWidth, r.Header.Get("Sec-CH-Width")), + SecCHViewportWidth: client.selectField(options.SecCHViewportWidth, r.Header.Get("Sec-CH-Viewport-Width")), + Title: options.Title, + Referrer: client.selectField(options.Referrer, client.getReferrerFromHeaderOrQuery(r)), + ScreenWidth: options.ScreenWidth, + ScreenHeight: options.ScreenHeight, } } @@ -634,7 +638,7 @@ func (client *Client) performPost(url string, body interface{}, retry int) error if err := client.refreshToken(); err != nil { if client.logger != nil { - client.logger.Printf("error refreshing token: %s", err) + client.logger.Error("error refreshing token", "err", err) } return errors.New(fmt.Sprintf("error refreshing token (attempt %d/%d): %s", client.requestRetries-retry, client.requestRetries, err)) @@ -671,7 +675,7 @@ func (client *Client) performPost(url string, body interface{}, retry int) error if err := client.refreshToken(); err != nil { if client.logger != nil { - client.logger.Printf("error refreshing token: %s", err) + client.logger.Error("error refreshing token", "err", err) } return errors.New(fmt.Sprintf("error refreshing token (attempt %d/%d): %s", client.requestRetries-retry, client.requestRetries, err)) @@ -698,7 +702,7 @@ func (client *Client) performGet(url string, retry int, result interface{}) erro if err := client.refreshToken(); err != nil { if client.logger != nil { - client.logger.Printf("error refreshing token: %s", err) + client.logger.Error("error refreshing token", "err", err) } return errors.New(fmt.Sprintf("error refreshing token (attempt %d/%d): %s", client.requestRetries-retry, client.requestRetries, err)) @@ -730,7 +734,7 @@ func (client *Client) performGet(url string, retry int, result interface{}) erro if err := client.refreshToken(); err != nil { if client.logger != nil { - client.logger.Printf("error refreshing token: %s", err) + client.logger.Error("error refreshing token", "err", err) } return errors.New(fmt.Sprintf("error refreshing token (attempt %d/%d): %s", client.requestRetries-retry, client.requestRetries, err)) diff --git a/client_test.go b/pkg/client_test.go similarity index 98% rename from client_test.go rename to pkg/client_test.go index e2139c6..7749432 100644 --- a/client_test.go +++ b/pkg/client_test.go @@ -1,4 +1,4 @@ -package pirsch +package pkg import ( "fmt" diff --git a/types.go b/pkg/types.go similarity index 92% rename from types.go rename to pkg/types.go index 41f2e79..6b6d708 100644 --- a/types.go +++ b/pkg/types.go @@ -1,4 +1,4 @@ -package pirsch +package pkg import ( "github.com/emvi/null" @@ -23,23 +23,29 @@ const ( // Use one of the constants ScaleDay, ScaleWeek, ScaleMonth, ScaleYear. type Scale string -// Hit are the parameters to send a page hit to Pirsch. -type Hit struct { - Hostname string - URL string `json:"url"` - IP string `json:"ip"` - UserAgent string `json:"user_agent"` - AcceptLanguage string `json:"accept_language"` - Title string `json:"title"` - Referrer string `json:"referrer"` - ScreenWidth int `json:"screen_width"` - ScreenHeight int `json:"screen_height"` +// PageView are the parameters to send a page hit to Pirsch. +type PageView struct { + Hostname string + URL string `json:"url"` + IP string `json:"ip"` + UserAgent string `json:"user_agent"` + AcceptLanguage string `json:"accept_language"` + SecCHUA string `json:"sec_ch_ua"` + SecCHUAMobile string `json:"sec_ch_ua_mobile"` + SecCHUAPlatform string `json:"sec_ch_ua_platform"` + SecCHUAPlatformVersion string `json:"sec_ch_ua_platform_version"` + SecCHWidth string `json:"sec_ch_width"` + SecCHViewportWidth string `json:"sec_ch_viewport_width"` + Title string `json:"title"` + Referrer string `json:"referrer"` + ScreenWidth int `json:"screen_width"` + ScreenHeight int `json:"screen_height"` } // Event represents a single data point for custom events. -// It's basically the same as Hit, but with some additional fields (event name, time, and meta fields). +// It's basically the same as PageView, but with some additional fields (event name, time, and meta fields). type Event struct { - Hit + PageView Name string `json:"event_name"` DurationSeconds int `json:"event_duration"` Metadata map[string]string `json:"event_meta"`