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{}