| // 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" |
| |
| "fuchsia.googlesource.com/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 |
| } |
| } |
| } |