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  }