github.com/tilt-dev/wat@v0.0.2-0.20180626175338-9349b638e250/cli/analytics/analytics.go (about) 1 package analytics 2 3 import ( 4 "fmt" 5 6 "time" 7 8 "net/http" 9 10 "encoding/json" 11 12 "bytes" 13 14 "os" 15 16 "context" 17 18 "os/exec" 19 20 "crypto/md5" 21 22 "github.com/spf13/cobra" 23 ) 24 25 const statsEndpt = "https://events.windmill.build/report" 26 const contentType = "Content-Type" 27 const contentTypeJson = "application/json" 28 const statsTimeout = time.Minute 29 30 // keys for request to stats server 31 const ( 32 keyDuration = "duration" 33 keyName = "name" 34 keyUser = "user" 35 ) 36 37 var cli = &http.Client{Timeout: statsTimeout} 38 39 func Init(appName string) (Analytics, *cobra.Command, error) { 40 a := NewRemoteAnalytics(appName) 41 c, err := initCLI() 42 if err != nil { 43 return nil, nil, err 44 } 45 46 return a, c, nil 47 } 48 49 type Analytics interface { 50 Count(name string, tags map[string]string, n int) 51 Incr(name string, tags map[string]string) 52 Timer(name string, dur time.Duration, tags map[string]string) 53 } 54 55 type RemoteAnalytics struct { 56 Cli *http.Client 57 App string 58 Url string 59 UserId string 60 OptedIn bool 61 } 62 63 func hashMd5(in []byte) string { 64 h := md5.New() 65 return fmt.Sprintf("%x", h.Sum(in)) 66 } 67 68 // getUserHash returns a unique identifier for this user by hashing `uname -a` 69 func getUserId() string { 70 ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) 71 defer cancel() 72 cmd := exec.CommandContext(ctx, "uname", "-a") 73 out, err := cmd.Output() 74 if err != nil || ctx.Err() != nil { 75 // Something went wrong, but ¯\_(ツ)_/¯ 76 return "anon" 77 } 78 return hashMd5(out) 79 } 80 81 func NewRemoteAnalytics(appName string) *RemoteAnalytics { 82 optedIn := optedIn() 83 return newRemoteAnalytics(cli, appName, statsEndpt, getUserId(), optedIn) 84 } 85 86 func newRemoteAnalytics(cli *http.Client, app, url, userId string, optedIn bool) *RemoteAnalytics { 87 return &RemoteAnalytics{Cli: cli, App: app, Url: url, UserId: userId, OptedIn: optedIn} 88 } 89 90 func (a *RemoteAnalytics) namespaced(name string) string { 91 return fmt.Sprintf("%s.%s", a.App, name) 92 } 93 func (a *RemoteAnalytics) baseReqBody(name string, tags map[string]string) map[string]interface{} { 94 req := map[string]interface{}{keyName: a.namespaced(name), keyUser: a.UserId} 95 for k, v := range tags { 96 req[k] = v 97 } 98 return req 99 } 100 101 func (a *RemoteAnalytics) makeReq(reqBody map[string]interface{}) (*http.Request, error) { 102 j, err := json.Marshal(reqBody) 103 if err != nil { 104 return nil, fmt.Errorf("json.Marshal: %v\n", err) 105 } 106 reader := bytes.NewReader(j) 107 108 req, err := http.NewRequest(http.MethodPost, a.Url, reader) 109 if err != nil { 110 return nil, fmt.Errorf("http.NewRequest: %v\n", err) 111 } 112 req.Header.Add(contentType, contentTypeJson) 113 114 return req, nil 115 } 116 117 func (a *RemoteAnalytics) Count(name string, tags map[string]string, n int) { 118 if !a.OptedIn { 119 return 120 } 121 122 go a.count(name, tags, n) 123 } 124 125 func (a *RemoteAnalytics) count(name string, tags map[string]string, n int) { 126 req, err := a.countReq(name, tags, n) 127 if err != nil { 128 // Stat reporter can't return errs, just print it. 129 fmt.Fprintf(os.Stderr, "[analytics] %v\n", err) 130 return 131 } 132 133 resp, err := a.Cli.Do(req) 134 if err != nil { 135 fmt.Fprintf(os.Stderr, "[analytics] http.Post: %v\n", err) 136 return 137 } 138 if resp.StatusCode != 200 { 139 fmt.Fprintf(os.Stderr, "[analytics] http.Post returned status: %s\n", resp.Status) 140 } 141 } 142 143 func (a *RemoteAnalytics) countReq(name string, tags map[string]string, n int) (*http.Request, error) { 144 // TODO: include n 145 return a.makeReq(a.baseReqBody(name, tags)) 146 } 147 148 func (a *RemoteAnalytics) Incr(name string, tags map[string]string) { 149 if !a.OptedIn { 150 return 151 } 152 a.Count(name, tags, 1) 153 } 154 155 func (a *RemoteAnalytics) Timer(name string, dur time.Duration, tags map[string]string) { 156 if !a.OptedIn { 157 return 158 } 159 160 go a.timer(name, dur, tags) 161 162 } 163 func (a *RemoteAnalytics) timer(name string, dur time.Duration, tags map[string]string) { 164 req, err := a.timerReq(name, dur, tags) 165 if err != nil { 166 // Stat reporter can't return errs, just print it. 167 fmt.Fprintf(os.Stderr, "[analytics] %v\n", err) 168 return 169 } 170 171 resp, err := a.Cli.Do(req) 172 if err != nil { 173 fmt.Fprintf(os.Stderr, "[analytics] http.Post: %v\n", err) 174 return 175 } 176 if resp.StatusCode != 200 { 177 fmt.Fprintf(os.Stderr, "[analytics] http.Post returned status: %s\n", resp.Status) 178 } 179 180 } 181 182 func (a *RemoteAnalytics) timerReq(name string, dur time.Duration, tags map[string]string) (*http.Request, error) { 183 reqBody := a.baseReqBody(name, tags) 184 reqBody[keyDuration] = dur 185 return a.makeReq(reqBody) 186 } 187 188 type MemoryAnalytics struct { 189 Counts []CountEvent 190 Timers []TimeEvent 191 } 192 193 type CountEvent struct { 194 name string 195 tags map[string]string 196 n int 197 } 198 199 type TimeEvent struct { 200 name string 201 tags map[string]string 202 dur time.Duration 203 } 204 205 func NewMemoryAnalytics() *MemoryAnalytics { 206 return &MemoryAnalytics{} 207 } 208 209 func (a *MemoryAnalytics) Count(name string, tags map[string]string, n int) { 210 a.Counts = append(a.Counts, CountEvent{name: name, tags: tags, n: n}) 211 } 212 213 func (a *MemoryAnalytics) Incr(name string, tags map[string]string) { 214 a.Count(name, tags, 1) 215 } 216 217 func (a *MemoryAnalytics) Timer(name string, dur time.Duration, tags map[string]string) { 218 a.Timers = append(a.Timers, TimeEvent{name: name, dur: dur, tags: tags}) 219 } 220 221 var _ Analytics = &RemoteAnalytics{} 222 var _ Analytics = &MemoryAnalytics{}