blob: 3f136a4b1004b9f74a55d77ef09a3faa83a8ab28 [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package analytics_util provides functions to send google analytics
package analytics_util
import (
"fmt"
"net/http"
"net/url"
"runtime"
"strconv"
"sync"
"time"
"go.fuchsia.dev/jiri/version"
)
var Version = "1.0v"
var analyticsUrl = "https://www.google-analytics.com/collect"
var CollectedData = `When opted in, jiri collects the following anonymized data in order to improve the user experience:
1. Tracks the commands that user runs.
2. Tracks the flags and their values passed with the commands. It does not track values for string flags unless they are true/false/number or following allowed values:
- always
- auto
- never
3. Creates a uuid for each jiri repository and sends that to track the session and user workflow.
4. Tracks user's operating system and its architecture.
5. Tracks the time taken by a command to complete.
6. Tracks jiri version.
7. Tracks time between subsequent jiri updates if more than 30 mins and less than 2 weeks.`
var customDimensionMapping map[string]string
func init() {
customDimensionMapping = make(map[string]string)
customDimensionMapping["os"] = "cd1"
customDimensionMapping["flags"] = "cd2"
}
type AnayticsObject interface {
send(as *AnalyticsSession)
}
type Event struct {
Category string
Action string
Label string
Value int64
CustomDimension map[string]string
}
type UserTiming struct {
Category string
Variable string
Label string
Timing int64
}
type AnalyticsSession struct {
enabled bool
tid string
cid string
objects map[int]JiriObject
sending map[int]bool
nextId int
lock *sync.Mutex
slock *sync.RWMutex
}
func (e Event) send(as *AnalyticsSession) {
params := make(map[string]string)
params["t"] = "event"
params["ec"] = e.Category
params["ea"] = e.Action
if e.Label != "" {
params["el"] = e.Label
}
if e.Value != 0 {
params["ev"] = strconv.FormatInt(e.Value, 10)
}
as.sendAnalytic(params, e.CustomDimension)
}
func (ut UserTiming) send(as *AnalyticsSession) {
params := make(map[string]string)
params["t"] = "timing"
params["utc"] = ut.Category
if ut.Variable != "" {
params["utv"] = ut.Variable
}
if ut.Label != "" {
params["utl"] = ut.Label
}
params["utt"] = strconv.FormatInt(ut.Timing, 10)
as.sendAnalytic(params, nil)
}
type JiriObject interface {
AnalyticsObject() AnayticsObject
Done()
}
func NewAnalyticsSession(enabled bool, tid string, cid string) *AnalyticsSession {
return &AnalyticsSession{
enabled: enabled,
tid: tid,
cid: cid,
objects: make(map[int]JiriObject),
sending: make(map[int]bool),
nextId: 0,
lock: &sync.Mutex{},
slock: &sync.RWMutex{},
}
}
func (as *AnalyticsSession) sendAnalytic(params, cds map[string]string) {
if !as.enabled {
return
}
val := url.Values{}
val.Add("v", "1")
val.Add("tid", as.tid)
val.Add("cid", as.cid)
v := version.FormattedVersion()
if v == "" {
v = "test"
}
val.Add("av", v)
val.Add("an", "jiri")
val.Add(customDimensionMapping["os"], fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH))
for k, v := range cds {
val.Add(customDimensionMapping[k], v)
}
for k, v := range params {
val.Add(k, v)
}
http.PostForm(analyticsUrl, val)
}
func (as *AnalyticsSession) Add(obj JiriObject) int {
if !as.enabled {
return -1
}
as.lock.Lock()
defer as.lock.Unlock()
as.objects[as.nextId] = obj
as.nextId++
return as.nextId - 1
}
func (as *AnalyticsSession) AddCommand(name string, flags map[string]string) int {
if !as.enabled {
return -1
}
return as.Add(newCommand(name, flags))
}
func (as *AnalyticsSession) AddCommandExecutionTiming(name string, timing time.Duration) int {
if !as.enabled {
return -1
}
return as.Add(newCommandExecutionTiming(name, timing))
}
func (as *AnalyticsSession) Send(id int) {
if !as.enabled {
return
}
as.slock.Lock()
defer as.slock.Unlock()
as.sending[id] = true
go func() {
as.lock.Lock()
defer as.lock.Unlock()
defer func() {
as.slock.Lock()
defer as.slock.Unlock()
delete(as.sending, id)
}()
if v, ok := as.objects[id]; ok {
delete(as.objects, id)
gaobject := v.AnalyticsObject()
gaobject.send(as)
}
}()
}
func (as *AnalyticsSession) Done(id int) {
if !as.enabled {
return
}
if v, ok := as.objects[id]; ok {
v.Done()
as.Send(id)
}
}
func (as *AnalyticsSession) SendAllAndWaitToFinish() {
if !as.enabled {
return
}
as.lock.Lock()
for k := range as.objects {
as.Send(k)
}
as.lock.Unlock()
for {
as.slock.RLock()
l := len(as.sending)
as.slock.RUnlock()
if l > 0 {
time.Sleep(100 * time.Microsecond)
} else {
break
}
}
}