From d5e06fe0c24ab3745c6aa27bcf606f8aaf5a4d23 Mon Sep 17 00:00:00 2001 From: Casey Primozic Date: Fri, 30 Aug 2019 18:34:31 -0700 Subject: [PATCH 1/2] Add support for pulling realtime Google Analytics metrics * Add config option `enableRealtime` that, if set to true, will cause realtime metrics to be displayed above the historicaly view counts for all view IDs * Add in the v3 Google API client and construct a service for it conditionally if realtime metrics are enabled * Update google analytics data pulling code to retrieve realtime metrics using the v3 client if realtime metrics are enabled in settings * Update table generation code to display fetched realtime metrics if they are available --- modules/googleanalytics/client.go | 95 ++++++++++++++++++++--------- modules/googleanalytics/display.go | 56 +++++++++++++---- modules/googleanalytics/settings.go | 14 +++-- 3 files changed, 118 insertions(+), 47 deletions(-) diff --git a/modules/googleanalytics/client.go b/modules/googleanalytics/client.go index f84b047b..557378c0 100644 --- a/modules/googleanalytics/client.go +++ b/modules/googleanalytics/client.go @@ -1,93 +1,132 @@ package googleanalytics import ( - "net/http" + "fmt" "io/ioutil" "log" - "fmt" + "net/http" "time" "github.com/wtfutil/wtf/utils" "golang.org/x/oauth2" "golang.org/x/oauth2/google" - ga "google.golang.org/api/analyticsreporting/v4" + gaV3 "google.golang.org/api/analytics/v3" + gaV4 "google.golang.org/api/analyticsreporting/v4" ) type websiteReport struct { - Name string - Report *ga.GetReportsResponse + Name string + Report *gaV4.GetReportsResponse + RealtimeReport *gaV3.RealtimeData } -func (widget *Widget) Fetch() ([]websiteReport) { +func (widget *Widget) Fetch() []websiteReport { secretPath, err := utils.ExpandHomeDir(widget.settings.secretFile) if err != nil { log.Fatalf("Unable to parse secretFile path") } - service, err := makeReportService(secretPath) + serviceV4, err := makeReportServiceV4(secretPath) if err != nil { - log.Fatalf("Unable to create Google Analytics Reporting Service") + log.Fatalf("Unable to create v3 Google Analytics Reporting Service") } - visitorsDataArray := getReports(service, widget.settings.viewIds, widget.settings.months) + var serviceV3 *gaV3.Service + if widget.settings.enableRealtime { + serviceV3, err = makeReportServiceV3(secretPath) + if err != nil { + log.Fatalf("Unable to create v3 Google Analytics Reporting Service") + } + } + + visitorsDataArray := getReports( + serviceV4, widget.settings.viewIds, widget.settings.months, serviceV3, + ) return visitorsDataArray } -func makeReportService(secretPath string) (*ga.Service, error) { +func buildNetClient(secretPath string) *http.Client { clientSecret, err := ioutil.ReadFile(secretPath) if err != nil { log.Fatalf("Unable to read secretPath. %v", err) } - jwtConfig, err := google.JWTConfigFromJSON(clientSecret, ga.AnalyticsReadonlyScope) + jwtConfig, err := google.JWTConfigFromJSON(clientSecret, gaV4.AnalyticsReadonlyScope) if err != nil { log.Fatalf("Unable to get config from JSON. %v", err) } - var netClient *http.Client - netClient = jwtConfig.Client(oauth2.NoContext) - svc, err := ga.New(netClient) + return jwtConfig.Client(oauth2.NoContext) +} + +func makeReportServiceV3(secretPath string) (*gaV3.Service, error) { + var netClient = buildNetClient(secretPath) + svc, err := gaV3.New(netClient) if err != nil { - log.Fatalf("Failed to create Google Analytics Reporting Service") + log.Fatalf("Failed to create v3 Google Analytics Reporting Service") } return svc, err } -func getReports(service *ga.Service, viewIds map[string]interface{}, displayedMonths int) ([]websiteReport) { - startDate := fmt.Sprintf("%s-01", time.Now().AddDate(0, -displayedMonths+1, 0).Format("2006-01")) - var websiteReports []websiteReport = nil +func makeReportServiceV4(secretPath string) (*gaV4.Service, error) { + var netClient = buildNetClient(secretPath) + svc, err := gaV4.New(netClient) + if err != nil { + log.Fatalf("Failed to create v4 Google Analytics Reporting Service") + } - for website, viewId := range viewIds { + return svc, err +} + +func getReports( + serviceV4 *gaV4.Service, viewIds map[string]interface{}, displayedMonths int, serviceV3 *gaV3.Service, +) []websiteReport { + startDate := fmt.Sprintf("%s-01", time.Now().AddDate(0, -displayedMonths+1, 0).Format("2006-01")) + var websiteReports []websiteReport + + for website, viewID := range viewIds { // For custom queries: https://ga-dev-tools.appspot.com/dimensions-metrics-explorer/ - req := &ga.GetReportsRequest{ - ReportRequests: []*ga.ReportRequest{ + req := &gaV4.GetReportsRequest{ + ReportRequests: []*gaV4.ReportRequest{ { - ViewId: viewId.(string), - DateRanges: []*ga.DateRange{ + ViewId: viewID.(string), + DateRanges: []*gaV4.DateRange{ {StartDate: startDate, EndDate: "today"}, }, - Metrics: []*ga.Metric{ + Metrics: []*gaV4.Metric{ {Expression: "ga:sessions"}, }, - Dimensions: []*ga.Dimension{ + Dimensions: []*gaV4.Dimension{ {Name: "ga:month"}, }, }, }, } - response, err := service.Reports.BatchGet(req).Do() + response, err := serviceV4.Reports.BatchGet(req).Do() if err != nil { - log.Fatalf("GET request to analyticsreporting/v4 returned error with viewID: %s", viewId) + log.Fatalf("GET request to analyticsreporting/v4 returned error with viewID: %s", viewID) } if response.HTTPStatusCode != 200 { log.Fatalf("Did not get expected HTTP response code") } - report := websiteReport{Name: website, Report: response,} + report := websiteReport{Name: website, Report: response} + if serviceV3 != nil { + report.RealtimeReport = getLiveCount(serviceV3, viewID.(string)) + } websiteReports = append(websiteReports, report) } return websiteReports } + +func getLiveCount(service *gaV3.Service, viewID string) *gaV3.RealtimeData { + res, err := service.Data.Realtime.Get("ga:"+viewID, "rt:activeUsers").Do() + if err != nil { + log.Fatalf("Failed to fetch realtime data for view ID %s: %v", viewID, err) + } + + return res +} diff --git a/modules/googleanalytics/display.go b/modules/googleanalytics/display.go index 43e109ed..4f367634 100644 --- a/modules/googleanalytics/display.go +++ b/modules/googleanalytics/display.go @@ -2,12 +2,40 @@ package googleanalytics import ( "fmt" - "time" "strings" + "time" ) -func (widget *Widget) createTable(websiteReports []websiteReport) (string) { - content := widget.createHeader() +func (widget *Widget) createTable(websiteReports []websiteReport) string { + content := "" + + if len(websiteReports) == 0 { + return content + } + + if websiteReports[0].RealtimeReport != nil { + content += "Realtime Visitor Counts\n" + for _, websiteReport := range websiteReports { + websiteRow := fmt.Sprintf(" %-20s", websiteReport.Name) + + if websiteReport.RealtimeReport == nil { + websiteRow += fmt.Sprintf("No data found for given ViewId.") + } else { + if len(websiteReport.RealtimeReport.Rows) == 0 { + websiteRow += "-" + } else { + websiteRow += fmt.Sprintf("%-10s", websiteReport.RealtimeReport.Rows[0][0]) + } + } + + content += websiteRow + "\n" + } + + content += "\n" + content += "Historical Visitor Counts\n" + } + + content += widget.createHeader() for _, websiteReport := range websiteReports { websiteRow := "" @@ -19,7 +47,7 @@ func (widget *Widget) createTable(websiteReports []websiteReport) (string) { // Fill in requested months with no data from query if noDataMonth > 0 { - websiteRow += strings.Repeat("- ", noDataMonth) + websiteRow += strings.Repeat("- ", noDataMonth) } if reportRows == nil { @@ -33,22 +61,24 @@ func (widget *Widget) createTable(websiteReports []websiteReport) (string) { } } } + content += websiteRow + "\n" } } + return content } -func (widget *Widget) createHeader() (string) { - // Creates the table header of consisting of Months +func (widget *Widget) createHeader() string { + // Creates the table header of consisting of Months currentMonth := int(time.Now().Month()) - widgetStartMonth := currentMonth-widget.settings.months+1 - header := " " + widgetStartMonth := currentMonth - widget.settings.months + 1 + header := " " - for i := widgetStartMonth; i < currentMonth+1; i++ { - header += fmt.Sprintf("%-10s", time.Month(i)) - } - header += "\n" + for i := widgetStartMonth; i < currentMonth+1; i++ { + header += fmt.Sprintf("%-10s", time.Month(i)) + } + header += "\n" - return header + return header } diff --git a/modules/googleanalytics/settings.go b/modules/googleanalytics/settings.go index f785800c..cd387b5f 100644 --- a/modules/googleanalytics/settings.go +++ b/modules/googleanalytics/settings.go @@ -10,9 +10,10 @@ const defaultTitle = "Google Analytics" type Settings struct { common *cfg.Common - months int - secretFile string `help:"Your Google client secret JSON file." values:"A string representing a file path to the JSON secret file."` - viewIds map[string]interface{} + months int + secretFile string `help:"Your Google client secret JSON file." values:"A string representing a file path to the JSON secret file."` + viewIds map[string]interface{} + enableRealtime bool } func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings { @@ -20,9 +21,10 @@ func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *co settings := Settings{ common: cfg.NewCommonSettingsFromModule(name, defaultTitle, ymlConfig, globalConfig), - months: ymlConfig.UInt("months"), - secretFile: ymlConfig.UString("secretFile"), - viewIds: ymlConfig.UMap("viewIds"), + months: ymlConfig.UInt("months"), + secretFile: ymlConfig.UString("secretFile"), + viewIds: ymlConfig.UMap("viewIds"), + enableRealtime: ymlConfig.UBool("enableRealtime", false), } return &settings From a56c1fa9231a8bd8b90a1d2286cf1d8922f19f94 Mon Sep 17 00:00:00 2001 From: Casey Primozic Date: Fri, 30 Aug 2019 18:46:43 -0700 Subject: [PATCH 2/2] Improve real time data fetch error message * Provide link to enroll in the real time data beta --- modules/googleanalytics/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/googleanalytics/client.go b/modules/googleanalytics/client.go index 557378c0..bbce4d59 100644 --- a/modules/googleanalytics/client.go +++ b/modules/googleanalytics/client.go @@ -125,7 +125,7 @@ func getReports( func getLiveCount(service *gaV3.Service, viewID string) *gaV3.RealtimeData { res, err := service.Data.Realtime.Get("ga:"+viewID, "rt:activeUsers").Do() if err != nil { - log.Fatalf("Failed to fetch realtime data for view ID %s: %v", viewID, err) + log.Fatalf("Failed to fetch real time data for view ID %s: %v. Have you enrolled in the real time beta? If not, do so here: https://docs.google.com/forms/d/1qfRFysCikpgCMGqgF3yXdUyQW4xAlLyjKuOoOEFN2Uw/viewform", viewID, err) } return res