mirror of
https://github.com/kopia/kopia.git
synced 2026-01-24 22:38:00 -05:00
169 lines
4.9 KiB
Go
169 lines
4.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/jpillora/go-ogle-analytics"
|
|
"github.com/kopia/kopia/internal/ospath"
|
|
"github.com/kopia/repo"
|
|
)
|
|
|
|
const (
|
|
googleAnalyticsID = "UA-256960-23"
|
|
|
|
maxAnalyticsReportTime = 1 * time.Second // do not block goroutine for more than this much time
|
|
)
|
|
|
|
var (
|
|
clientIDFile = app.Flag("client-id-file", "Path to client ID file, which enables anonymous usage reporting if present").Default(filepath.Join(ospath.ConfigDir(), "client_id.txt")).String()
|
|
analyticsConsent = app.Flag("analytics-consent", "Consent to send analytics").Default("ask").Enum("agree", "disagree", "ask")
|
|
|
|
globalGAClient *ga.Client
|
|
gaClientOnce sync.Once
|
|
)
|
|
|
|
func analyticsUserAgent() string {
|
|
return "kopia-" + repo.BuildVersion + "-" + runtime.GOARCH + "-" + runtime.GOOS
|
|
}
|
|
|
|
func enableAnalytics(clientID string) {
|
|
f, err := os.Create(*clientIDFile)
|
|
if err != nil {
|
|
log.Warningf("unable to write client ID: %v", err)
|
|
return
|
|
}
|
|
|
|
defer f.Close() //nolint:errcheck
|
|
fmt.Fprintf(f, "%v", clientID) //nolint:errcheck
|
|
}
|
|
|
|
func disableAnalytics() {
|
|
// the user does NOT want to provide analytics consent, delete client ID file, if any.
|
|
os.Remove(*clientIDFile) //nolint:errcheck
|
|
}
|
|
|
|
func displayAnalyticsConsentPrompt(clientID string) bool {
|
|
var consent string
|
|
fmt.Println()
|
|
fmt.Println("Help improve Kopia by sharing anonymous usage statistics with developers:")
|
|
fmt.Println()
|
|
fmt.Println("The information includes:")
|
|
fmt.Println(" Current build version: ", analyticsUserAgent())
|
|
fmt.Println(" Random anonymous identifier: ", clientID)
|
|
fmt.Println(" Type and version of storage: ", "(e.g. 'gcs-v1', 's3-v1')")
|
|
fmt.Println(" Name of the feature used: ", "(e.g. 'snapshot create', 'policy set')")
|
|
fmt.Println()
|
|
fmt.Println("No personal information (user names, host names, filenames, etc.) is ever sent:")
|
|
fmt.Println()
|
|
fmt.Print("Do you agree? (y/n) ")
|
|
fmt.Scanf("%s", &consent) //nolint:errcheck
|
|
if !strings.HasPrefix(strings.ToLower(consent), "y") {
|
|
return false
|
|
}
|
|
fmt.Println("Thank you for sharing!")
|
|
return true
|
|
}
|
|
|
|
func promptForAnalyticsConsent() {
|
|
// generate 64-bit ID as 16 hex digits.
|
|
x := make([]byte, 8)
|
|
rand.Read(x) //nolint:errcheck
|
|
clientID := fmt.Sprintf("%x", x)
|
|
|
|
switch *analyticsConsent {
|
|
case "disagree":
|
|
disableAnalytics()
|
|
|
|
case "agree":
|
|
enableAnalytics(clientID)
|
|
|
|
case "ask":
|
|
if displayAnalyticsConsentPrompt(clientID) {
|
|
enableAnalytics(clientID)
|
|
} else {
|
|
disableAnalytics()
|
|
}
|
|
}
|
|
}
|
|
|
|
// initGAClient sets up Google Analytics reporting for this session.
|
|
func initGAClient() *ga.Client {
|
|
gaClientOnce.Do(func() {
|
|
f, err := os.Open(*clientIDFile)
|
|
if os.IsNotExist(err) {
|
|
log.Debugf("not reporting usage - %v does not exist", *clientIDFile)
|
|
return
|
|
}
|
|
if err != nil {
|
|
log.Warningf("unable to open client ID: %v", err)
|
|
}
|
|
defer f.Close() //nolint:errcheck
|
|
|
|
var clientID string
|
|
if _, serr := fmt.Fscanf(f, "%s", &clientID); serr != nil {
|
|
log.Debugf("not reporting usage - invalid client ID file: %v", *clientIDFile)
|
|
return
|
|
}
|
|
|
|
client, err := ga.NewClient(googleAnalyticsID)
|
|
if err != nil {
|
|
// this should not happen because the UA is valid.
|
|
panic("unable to initialize GA client: " + err.Error())
|
|
}
|
|
|
|
userAgent := analyticsUserAgent()
|
|
log.Debugf("analytics userAgent: %q, clientID: %q", userAgent, clientID)
|
|
|
|
globalGAClient = client.
|
|
ClientID(clientID).
|
|
UserAgentOverride(userAgent).
|
|
AnonymizeIP(true)
|
|
})
|
|
return globalGAClient
|
|
}
|
|
|
|
// reportStartupTime reports startup time.
|
|
func reportStartupTime(storageType string, formatVersion int, startupDuration time.Duration) {
|
|
if gaClient := initGAClient(); gaClient != nil && *analyticsConsent != "no" {
|
|
log.Debugf("reporting startup duration %v", startupDuration)
|
|
go gaClient.Send( //nolint:errcheck
|
|
ga.NewEvent("initialize", fmt.Sprintf("%v-v%v", storageType, formatVersion)).
|
|
Value(startupDuration.Nanoseconds() / 1e6))
|
|
}
|
|
}
|
|
|
|
// reportSubcommandFinished reports a single subcommand usage.
|
|
func reportSubcommandFinished(commandType string, success bool, storageType string, formatVersion int, duration time.Duration) {
|
|
if gaClient := initGAClient(); gaClient != nil && *analyticsConsent != "no" {
|
|
log.Debugf("reporting command %v finished (success=%v, duration=%v)", commandType, success, duration)
|
|
quickOrIgnore(func() {
|
|
if success {
|
|
gaClient.Send(ga.NewEvent("command-success", commandType).Label(fmt.Sprintf("%v-v%v", storageType, formatVersion)).Value(duration.Nanoseconds() / 1e6)) //nolint:errcheck
|
|
} else {
|
|
gaClient.Send(ga.NewEvent("command-failed", commandType).Label(fmt.Sprintf("%v-v%v", storageType, formatVersion)).Value(duration.Nanoseconds() / 1e6)) //nolint:errcheck
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func quickOrIgnore(f func()) {
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
defer close(done)
|
|
f()
|
|
}()
|
|
|
|
select {
|
|
case <-time.After(maxAnalyticsReportTime):
|
|
case <-done:
|
|
}
|
|
}
|