From 6f7dc0c09391065a2190337c9be8ce4191955316 Mon Sep 17 00:00:00 2001 From: Marvin Blum Date: Thu, 20 Jan 2022 14:52:27 +0100 Subject: [PATCH 1/2] Added new hourly visitor statistics. --- CHANGELOG.md | 4 ++++ types.go | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f050b32..165ce1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.6.0 + +* added new hourly visitor statistics + ## 1.5.1 * added endpoint for total visitor statistics diff --git a/types.go b/types.go index 5e3d3bf..86f69c9 100644 --- a/types.go +++ b/types.go @@ -240,8 +240,12 @@ type ActiveVisitorsData struct { // VisitorHourStats is the result type for visitor statistics grouped by time of day. type VisitorHourStats struct { - Hour int `json:"hour"` - Visitors int `json:"visitors"` + Hour int `json:"hour"` + Visitors int `json:"visitors"` + Views int `json:"views"` + Sessions int `json:"sessions"` + Bounces int `json:"bounces"` + BounceRate float64 `json:"bounce_rate"` } // LanguageStats is the result type for language statistics. From 8851d1305c250c0c4b54b810b07f7648665c0ec1 Mon Sep 17 00:00:00 2001 From: Marvin Blum Date: Sun, 3 Apr 2022 12:42:10 +0200 Subject: [PATCH 2/2] Added hourly visitor statistics, listing events, os and browser versions, added filter options, updated return types with new and modified fields, improved error messages. --- CHANGELOG.md | 5 ++- client.go | 94 ++++++++++++++++++++++++++++++++++++++++++---------- types.go | 68 ++++++++++++++++++++++++++++++++----- 3 files changed, 140 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 165ce1a..28f18f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,10 @@ ## 1.6.0 -* added new hourly visitor statistics +* added hourly visitor statistics, listing events, os and browser versions +* added filter options +* updated return types with new and modified fields +* improved error messages ## 1.5.1 diff --git a/client.go b/client.go index cea956c..121b396 100644 --- a/client.go +++ b/client.go @@ -36,13 +36,16 @@ const ( conversionGoalsEndpoint = "/api/v1/statistics/goals" eventsEndpoint = "/api/v1/statistics/events" eventMetadataEndpoint = "/api/v1/statistics/event/meta" + listEventsEndpoint = "/api/v1/statistics/event/list" growthRateEndpoint = "/api/v1/statistics/growth" activeVisitorsEndpoint = "/api/v1/statistics/active" timeOfDayEndpoint = "/api/v1/statistics/hours" languageEndpoint = "/api/v1/statistics/language" referrerEndpoint = "/api/v1/statistics/referrer" osEndpoint = "/api/v1/statistics/os" + osVersionEndpoint = "/api/v1/statistics/os/version" browserEndpoint = "/api/v1/statistics/browser" + browserVersionEndpoint = "/api/v1/statistics/browser/version" countryEndpoint = "/api/v1/statistics/country" cityEndpoint = "/api/v1/statistics/city" platformEndpoint = "/api/v1/statistics/platform" @@ -384,6 +387,17 @@ func (client *Client) EventMetadata(filter *Filter) ([]EventStats, error) { return stats, nil } +// ListEvents returns a list of all events including metadata. +func (client *Client) ListEvents(filter *Filter) ([]EventListStats, error) { + stats := make([]EventListStats, 0) + + if err := client.performGet(client.getStatsRequestURL(listEventsEndpoint, filter), requestRetries, &stats); err != nil { + return nil, err + } + + return stats, nil +} + // Growth returns the growth rates for visitors, bounces, ... func (client *Client) Growth(filter *Filter) (*Growth, error) { growth := new(Growth) @@ -450,6 +464,17 @@ func (client *Client) OS(filter *Filter) ([]OSStats, error) { return stats, nil } +// OSVersions returns operating system version statistics. +func (client *Client) OSVersions(filter *Filter) ([]OSVersionStats, error) { + stats := make([]OSVersionStats, 0) + + if err := client.performGet(client.getStatsRequestURL(osVersionEndpoint, filter), requestRetries, &stats); err != nil { + return nil, err + } + + return stats, nil +} + // Browser returns browser statistics. func (client *Client) Browser(filter *Filter) ([]BrowserStats, error) { stats := make([]BrowserStats, 0) @@ -461,6 +486,17 @@ func (client *Client) Browser(filter *Filter) ([]BrowserStats, error) { return stats, nil } +// BrowserVersions returns browser version statistics. +func (client *Client) BrowserVersions(filter *Filter) ([]BrowserVersionStats, error) { + stats := make([]BrowserVersionStats, 0) + + if err := client.performGet(client.getStatsRequestURL(browserVersionEndpoint, filter), requestRetries, &stats); err != nil { + return nil, err + } + + return stats, nil +} + // Country returns country statistics. func (client *Client) Country(filter *Filter) ([]CountryStats, error) { stats := make([]CountryStats, 0) @@ -535,12 +571,8 @@ func (client *Client) getReferrerFromHeaderOrQuery(r *http.Request) string { func (client *Client) refreshToken() error { client.m.Lock() defer client.m.Unlock() - - // check token has expired or is about to expire soon - if client.expiresAt.After(time.Now().UTC().Add(-time.Minute)) { - return nil - } - + client.accessToken = "" + client.expiresAt = time.Time{} body := struct { ClientId string `json:"client_id"` ClientSecret string `json:"client_secret"` @@ -578,6 +610,24 @@ func (client *Client) refreshToken() error { } func (client *Client) performPost(url string, body interface{}, retry int) error { + client.m.RLock() + accessToken := client.accessToken + client.m.RUnlock() + + if retry > 0 && accessToken == "" { + client.waitBeforeNextRequest(retry) + + if err := client.refreshToken(); err != nil { + if client.logger != nil { + client.logger.Printf("error refreshing token: %s", err) + } + + return errors.New(fmt.Sprintf("error refreshing token (attempt %d/%d): %s", requestRetries-retry, requestRetries, err)) + } + + return client.performPost(url, body, retry-1) + } + reqBody, err := json.Marshal(body) if err != nil { @@ -600,16 +650,16 @@ func (client *Client) performPost(url string, body interface{}, retry int) error return err } - // refresh access token and retry on 401 - if retry > 0 && resp.StatusCode == http.StatusUnauthorized { - time.Sleep(time.Millisecond * time.Duration((requestRetries-retry)*100+50)) + // refresh access token and retry + if retry > 0 && resp.StatusCode != http.StatusOK { + client.waitBeforeNextRequest(retry) if err := client.refreshToken(); err != nil { if client.logger != nil { client.logger.Printf("error refreshing token: %s", err) } - return err + return errors.New(fmt.Sprintf("error refreshing token (attempt %d/%d): %s", requestRetries-retry, requestRetries, err)) } return client.performPost(url, body, retry-1) @@ -624,15 +674,19 @@ func (client *Client) performPost(url string, body interface{}, retry int) error } func (client *Client) performGet(url string, retry int, result interface{}) error { - if retry > 0 && client.accessToken == "" { - time.Sleep(time.Millisecond * time.Duration((requestRetries-retry)*100+50)) + client.m.RLock() + accessToken := client.accessToken + client.m.RUnlock() + + if retry > 0 && accessToken == "" { + client.waitBeforeNextRequest(retry) if err := client.refreshToken(); err != nil { if client.logger != nil { client.logger.Printf("error refreshing token: %s", err) } - return err + return errors.New(fmt.Sprintf("error refreshing token (attempt %d/%d): %s", requestRetries-retry, requestRetries, err)) } return client.performGet(url, retry-1, result) @@ -646,8 +700,8 @@ func (client *Client) performGet(url string, retry int, result interface{}) erro client.m.RLock() req.Header.Set("Authorization", "Bearer "+client.accessToken) - req.Header.Set("Content-Type", "application/json") client.m.RUnlock() + req.Header.Set("Content-Type", "application/json") c := http.Client{} resp, err := c.Do(req) @@ -655,16 +709,16 @@ func (client *Client) performGet(url string, retry int, result interface{}) erro return err } - // refresh access token and retry on 401 - if retry > 0 && resp.StatusCode == http.StatusUnauthorized { - time.Sleep(time.Millisecond * time.Duration((requestRetries-retry)*100+50)) + // refresh access token and retry + if retry > 0 && resp.StatusCode != http.StatusOK { + client.waitBeforeNextRequest(retry) if err := client.refreshToken(); err != nil { if client.logger != nil { client.logger.Printf("error refreshing token: %s", err) } - return err + return errors.New(fmt.Sprintf("error refreshing token (attempt %d/%d): %s", requestRetries-retry, requestRetries, err)) } return client.performGet(url, retry-1, result) @@ -728,3 +782,7 @@ func (client *Client) getStatsRequestURL(endpoint string, filter *Filter) string return u + "?" + v.Encode() } + +func (client *Client) waitBeforeNextRequest(retry int) { + time.Sleep(time.Second * time.Duration(requestRetries-retry)) +} diff --git a/types.go b/types.go index 86f69c9..87aa944 100644 --- a/types.go +++ b/types.go @@ -5,6 +5,24 @@ import ( "time" ) +const ( + // ScaleDay groups results by day. + ScaleDay = "day" + + // ScaleWeek groups results by week. + ScaleWeek = "week" + + // ScaleMonth groups results by month. + ScaleMonth = "month" + + // ScaleYear groups results by year. + ScaleYear = "year" +) + +// Scale is used to group results in the Filter. +// 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 @@ -36,6 +54,8 @@ type Filter struct { DomainID string `json:"id"` From time.Time `json:"from"` To time.Time `json:"to"` + Start int `json:"start,omitempty"` + Scale Scale `json:"scale,omitempty"` Path string `json:"path,omitempty"` Pattern string `json:"pattern,omitempty"` EntryPath string `json:"entry_path,omitempty"` @@ -51,6 +71,8 @@ type Filter struct { Browser string `json:"browser,omitempty"` Platform string `json:"platform,omitempty"` ScreenClass string `json:"screen_class,omitempty"` + ScreenWidth string `json:"screen_width,omitempty"` + ScreenHeight string `json:"screen_height,omitempty"` UTMSource string `json:"utm_source,omitempty"` UTMMedium string `json:"utm_medium,omitempty"` UTMCampaign string `json:"utm_campaign,omitempty"` @@ -85,8 +107,12 @@ type Domain struct { // TimeSpentStats is the time spent on the website or specific pages. type TimeSpentStats struct { - Day time.Time `json:"day"` + Day null.Time `json:"day"` + Week null.Time `json:"week"` + Month null.Time `json:"month"` + Year null.Time `json:"year"` Path string `json:"path"` + Title string `json:"title"` AverageTimeSpentSeconds int `json:"average_time_spent_seconds"` } @@ -132,12 +158,15 @@ type TotalVisitorStats struct { Views int `json:"views"` Sessions int `json:"sessions"` Bounces int `json:"bounces"` - BounceRate float64 `db:"bounce_rate" json:"bounce_rate"` + BounceRate float64 `json:"bounce_rate"` } // VisitorStats is the result type for visitor statistics. type VisitorStats struct { - Day time.Time `json:"day"` + Day null.Time `json:"day"` + Week null.Time `json:"week"` + Month null.Time `json:"month"` + Year null.Time `json:"year"` Visitors int `json:"visitors"` Views int `json:"views"` Sessions int `json:"sessions"` @@ -160,23 +189,23 @@ type PageStats struct { // EntryStats is the result type for entry page statistics. type EntryStats struct { - Path string `db:"entry_path" json:"path"` + Path string `json:"path"` Title string `json:"title"` Visitors int `json:"visitors"` Sessions int `json:"sessions"` Entries int `json:"entries"` - EntryRate float64 `db:"entry_rate" json:"entry_rate"` - AverageTimeSpentSeconds int `db:"average_time_spent_seconds" json:"average_time_spent_seconds"` + EntryRate float64 `json:"entry_rate"` + AverageTimeSpentSeconds int `json:"average_time_spent_seconds"` } // ExitStats is the result type for exit page statistics. type ExitStats struct { - Path string `db:"exit_path" json:"path"` + Path string `json:"path"` Title string `json:"title"` Visitors int `json:"visitors"` Sessions int `json:"sessions"` Exits int `json:"exits"` - ExitRate float64 `db:"exit_rate" json:"exit_rate"` + ExitRate float64 `json:"exit_rate"` } // ConversionGoal is a conversion goal as configured on the dashboard. @@ -204,6 +233,14 @@ type EventStats struct { MetaValue string `json:"meta_value"` } +// EventListStats is the result type for a custom event list. +type EventListStats struct { + Name string `json:"name"` + Meta map[string]string `json:"meta"` + Visitors int `json:"visitors"` + Count int `json:"count"` +} + // PageConversionsStats is the result type for page conversions. type PageConversionsStats struct { Visitors int `json:"visitors"` @@ -229,6 +266,7 @@ type Growth struct { // ActiveVisitorStats is the result type for active visitor statistics. type ActiveVisitorStats struct { Path string `json:"path"` + Title string `json:"title"` Visitors int `json:"visitors"` } @@ -272,12 +310,26 @@ type BrowserStats struct { Browser string `json:"browser"` } +// BrowserVersionStats is the result type for browser version statistics. +type BrowserVersionStats struct { + MetaStats + Browser string `json:"browser"` + BrowserVersion string `db:"browser_version" json:"browser_version"` +} + // OSStats is the result type for operating system statistics. type OSStats struct { MetaStats OS string `json:"os"` } +// OSVersionStats is the result type for operating system version statistics. +type OSVersionStats struct { + MetaStats + OS string `json:"os"` + OSVersion string `db:"os_version" json:"os_version"` +} + // ReferrerStats is the result type for referrer statistics. type ReferrerStats struct { Referrer string `json:"referrer"`