github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/analytics/client/async/client.go (about) 1 package async 2 3 import ( 4 "context" 5 "os" 6 "runtime/debug" 7 "sync" 8 "time" 9 10 "github.com/ActiveState/cli/internal/analytics" 11 ac "github.com/ActiveState/cli/internal/analytics/constants" 12 "github.com/ActiveState/cli/internal/analytics/dimensions" 13 "github.com/ActiveState/cli/internal/condition" 14 "github.com/ActiveState/cli/internal/config" 15 "github.com/ActiveState/cli/internal/constants" 16 "github.com/ActiveState/cli/internal/errs" 17 "github.com/ActiveState/cli/internal/logging" 18 "github.com/ActiveState/cli/internal/multilog" 19 "github.com/ActiveState/cli/internal/output" 20 "github.com/ActiveState/cli/internal/profile" 21 "github.com/ActiveState/cli/internal/rtutils/ptr" 22 "github.com/ActiveState/cli/internal/updater" 23 "github.com/ActiveState/cli/pkg/platform/authentication" 24 "github.com/ActiveState/cli/pkg/platform/model" 25 ) 26 27 // Client is the default analytics dispatcher, forwarding analytics events to the state-svc service 28 type Client struct { 29 svcModel *model.SvcModel 30 auth *authentication.Auth 31 output string 32 projectNameSpace string 33 eventWaitGroup *sync.WaitGroup 34 sessionToken string 35 updateTag string 36 closed bool 37 sequence int 38 ci bool 39 interactive bool 40 activestateCI bool 41 source string 42 } 43 44 var _ analytics.Dispatcher = &Client{} 45 46 func New(source string, svcModel *model.SvcModel, cfg *config.Instance, auth *authentication.Auth, out output.Outputer, projectNameSpace string) *Client { 47 a := &Client{ 48 eventWaitGroup: &sync.WaitGroup{}, 49 source: source, 50 } 51 52 o := string(output.PlainFormatName) 53 if out.Type() != "" { 54 o = string(out.Type()) 55 } 56 a.output = o 57 a.projectNameSpace = projectNameSpace 58 a.auth = auth 59 a.ci = condition.OnCI() 60 a.interactive = out.Config().Interactive 61 a.activestateCI = condition.InActiveStateCI() 62 63 if condition.InUnitTest() { 64 return a 65 } 66 67 a.svcModel = svcModel 68 69 a.sessionToken = cfg.GetString(ac.CfgSessionToken) 70 tag, ok := os.LookupEnv(constants.UpdateTagEnvVarName) 71 if !ok { 72 tag = cfg.GetString(updater.CfgUpdateTag) 73 } 74 a.updateTag = tag 75 76 return a 77 } 78 79 // Event logs an event to google analytics 80 func (a *Client) Event(category, action string, dims ...*dimensions.Values) { 81 a.EventWithLabel(category, action, "", dims...) 82 } 83 84 // EventWithLabel logs an event with a label to google analytics 85 func (a *Client) EventWithLabel(category, action, label string, dims ...*dimensions.Values) { 86 a.eventWithSourceAndLabel(category, action, a.source, label, dims...) 87 } 88 89 // EventWithSource logs an event with another source to google analytics. 90 // For example, log runtime events triggered by executors as coming from an executor instead of from 91 // State Tool. 92 func (a *Client) EventWithSource(category, action, source string, dims ...*dimensions.Values) { 93 a.eventWithSourceAndLabel(category, action, source, "", dims...) 94 } 95 96 func (a *Client) eventWithSourceAndLabel(category, action, source, label string, dims ...*dimensions.Values) { 97 err := a.sendEvent(category, action, source, label, dims...) 98 if err != nil { 99 multilog.Error("Error during analytics.sendEvent: %v", errs.JoinMessage(err)) 100 } 101 } 102 103 // Wait can be called to ensure that all events have been processed 104 func (a *Client) Wait() { 105 defer profile.Measure("analytics:Wait", time.Now()) 106 107 // we want Wait() to work for uninitialized Analytics 108 if a == nil { 109 return 110 } 111 a.eventWaitGroup.Wait() 112 } 113 114 func (a *Client) sendEvent(category, action, source, label string, dims ...*dimensions.Values) error { 115 if a.svcModel == nil { // this is only true on CI 116 return nil 117 } 118 119 if a.closed { 120 logging.Debug("Client is closed, not sending event") 121 return nil 122 } 123 124 userID := "" 125 if a.auth != nil && a.auth.UserID() != nil { 126 userID = string(*a.auth.UserID()) 127 } 128 129 dim := dimensions.NewDefaultDimensions(a.projectNameSpace, a.sessionToken, a.updateTag, a.auth) 130 dim.OutputType = &a.output 131 dim.UserID = &userID 132 dim.Sequence = ptr.To(a.sequence) 133 a.sequence++ 134 dim.CI = &a.ci 135 dim.Interactive = &a.interactive 136 dim.ActiveStateCI = &a.activestateCI 137 dim.Merge(dims...) 138 139 dimMarshalled, err := dim.Marshal() 140 if err != nil { 141 return errs.Wrap(err, "Could not marshal dimensions") 142 } 143 144 a.eventWaitGroup.Add(1) 145 go func() { 146 defer func() { handlePanics(recover(), debug.Stack()) }() 147 defer a.eventWaitGroup.Done() 148 149 if err := a.svcModel.AnalyticsEvent(context.Background(), category, action, source, label, string(dimMarshalled)); err != nil { 150 logging.Debug("Failed to report analytics event via state-svc: %s", errs.JoinMessage(err)) 151 } 152 }() 153 return nil 154 } 155 156 func handlePanics(err interface{}, stack []byte) { 157 if err == nil { 158 return 159 } 160 multilog.Error("Panic in client analytics: %v", err) 161 logging.Debug("Stack: %s", string(stack)) 162 } 163 164 func (a *Client) Close() { 165 a.Wait() 166 a.closed = true 167 }