github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/analytics/client/sync/client.go (about)

     1  package sync
     2  
     3  import (
     4  	"os"
     5  	"runtime/debug"
     6  	"strings"
     7  	"sync"
     8  
     9  	"github.com/ActiveState/cli/internal/analytics"
    10  	"github.com/ActiveState/cli/internal/analytics/client/sync/reporters"
    11  	anaConsts "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/installation/storage"
    18  	"github.com/ActiveState/cli/internal/instanceid"
    19  	"github.com/ActiveState/cli/internal/logging"
    20  	configMediator "github.com/ActiveState/cli/internal/mediators/config"
    21  	"github.com/ActiveState/cli/internal/multilog"
    22  	"github.com/ActiveState/cli/internal/osutils"
    23  	"github.com/ActiveState/cli/internal/output"
    24  	"github.com/ActiveState/cli/internal/rollbar"
    25  	"github.com/ActiveState/cli/internal/rtutils/ptr"
    26  	"github.com/ActiveState/cli/internal/singleton/uniqid"
    27  	"github.com/ActiveState/cli/internal/updater"
    28  	"github.com/ActiveState/cli/pkg/platform/authentication"
    29  	"github.com/ActiveState/cli/pkg/sysinfo"
    30  )
    31  
    32  type Reporter interface {
    33  	ID() string
    34  	Event(category, action, source, label string, dimensions *dimensions.Values) error
    35  }
    36  
    37  // Client instances send analytics events to GA and S3 endpoints without delay. It is only supposed to be used inside the `state-svc`.  All other processes should use the DefaultClient.
    38  type Client struct {
    39  	customDimensions *dimensions.Values
    40  	cfg              *config.Instance
    41  	eventWaitGroup   *sync.WaitGroup
    42  	sendReports      bool
    43  	reporters        []Reporter
    44  	sequence         int
    45  	auth             *authentication.Auth
    46  	source           string
    47  }
    48  
    49  var _ analytics.Dispatcher = &Client{}
    50  
    51  // New initializes the analytics instance with all custom dimensions known at this time
    52  func New(source string, cfg *config.Instance, auth *authentication.Auth, out output.Outputer) *Client {
    53  	a := &Client{
    54  		eventWaitGroup: &sync.WaitGroup{},
    55  		sendReports:    true,
    56  		auth:           auth,
    57  		source:         source,
    58  	}
    59  
    60  	installSource, err := storage.InstallSource()
    61  	if err != nil {
    62  		multilog.Error("Could not detect installSource: %s", errs.JoinMessage(err))
    63  	}
    64  
    65  	deviceID := uniqid.Text()
    66  
    67  	osName := sysinfo.OS().String()
    68  	osVersion := "unknown"
    69  	osvInfo, err := sysinfo.OSVersion()
    70  	if err != nil {
    71  		multilog.Log(logging.ErrorNoStacktrace, rollbar.Error)("Could not detect osVersion: %v", err)
    72  	}
    73  	if osvInfo != nil {
    74  		osVersion = osvInfo.Version
    75  	}
    76  
    77  	var sessionToken string
    78  	var tag string
    79  	if cfg != nil {
    80  		sessionToken = cfg.GetString(anaConsts.CfgSessionToken)
    81  		var ok bool
    82  		tag, ok = os.LookupEnv(constants.UpdateTagEnvVarName)
    83  		if !ok {
    84  			tag = cfg.GetString(updater.CfgUpdateTag)
    85  		}
    86  		a.cfg = cfg
    87  	}
    88  
    89  	a.readConfig()
    90  	configMediator.AddListener(constants.ReportAnalyticsConfig, a.readConfig)
    91  
    92  	userID := ""
    93  	if auth != nil && auth.UserID() != nil {
    94  		userID = string(*auth.UserID())
    95  	}
    96  
    97  	interactive := false
    98  	if out != nil {
    99  		interactive = out.Config().Interactive
   100  	}
   101  
   102  	customDimensions := &dimensions.Values{
   103  		Version:       ptr.To(constants.Version),
   104  		ChannelName:   ptr.To(constants.ChannelName),
   105  		OSName:        ptr.To(osName),
   106  		OSVersion:     ptr.To(osVersion),
   107  		InstallSource: ptr.To(installSource),
   108  		UniqID:        ptr.To(deviceID),
   109  		SessionToken:  ptr.To(sessionToken),
   110  		UpdateTag:     ptr.To(tag),
   111  		UserID:        ptr.To(userID),
   112  		Flags:         ptr.To(dimensions.CalculateFlags()),
   113  		InstanceID:    ptr.To(instanceid.ID()),
   114  		Command:       ptr.To(osutils.ExecutableName()),
   115  		Sequence:      ptr.To(0),
   116  		CI:            ptr.To(condition.OnCI()),
   117  		Interactive:   ptr.To(interactive),
   118  		ActiveStateCI: ptr.To(condition.InActiveStateCI()),
   119  	}
   120  
   121  	a.customDimensions = customDimensions
   122  
   123  	// Register reporters
   124  	if condition.InTest() {
   125  		logging.Debug("Using test reporter")
   126  		a.NewReporter(reporters.NewTestReporter(reporters.TestReportFilepath()))
   127  		logging.Debug("Using test reporter as instructed by env")
   128  	} else if v := os.Getenv(constants.AnalyticsLogEnvVarName); v != "" {
   129  		a.NewReporter(reporters.NewTestReporter(v))
   130  	} else {
   131  		a.NewReporter(reporters.NewPixelReporter())
   132  	}
   133  
   134  	return a
   135  }
   136  
   137  func (a *Client) readConfig() {
   138  	doNotReport := (!a.cfg.Closed() && !a.cfg.GetBool(constants.ReportAnalyticsConfig)) ||
   139  		strings.ToLower(os.Getenv(constants.DisableAnalyticsEnvVarName)) == "true"
   140  	a.sendReports = !doNotReport
   141  	logging.Debug("Sending Google Analytics reports? %v", a.sendReports)
   142  }
   143  
   144  func (a *Client) NewReporter(rep Reporter) {
   145  	a.reporters = append(a.reporters, rep)
   146  }
   147  
   148  func (a *Client) Wait() {
   149  	a.eventWaitGroup.Wait()
   150  }
   151  
   152  // Events returns a channel to feed eventData directly to the report loop
   153  func (a *Client) report(category, action, source, label string, dimensions *dimensions.Values) {
   154  	if !a.sendReports {
   155  		return
   156  	}
   157  
   158  	for _, reporter := range a.reporters {
   159  		if err := reporter.Event(category, action, source, label, dimensions); err != nil {
   160  			logging.Debug(
   161  				"Reporter failed: %s, category: %s, action: %s, error: %s",
   162  				reporter.ID(), category, action, errs.JoinMessage(err),
   163  			)
   164  		}
   165  	}
   166  }
   167  
   168  func (a *Client) Event(category, action string, dims ...*dimensions.Values) {
   169  	a.EventWithLabel(category, action, "", dims...)
   170  }
   171  
   172  func mergeDimensions(target *dimensions.Values, dims ...*dimensions.Values) *dimensions.Values {
   173  	actualDims := target.Clone()
   174  	for _, dim := range dims {
   175  		if dim == nil {
   176  			continue
   177  		}
   178  		actualDims.Merge(dim)
   179  	}
   180  	return actualDims
   181  }
   182  
   183  func (a *Client) EventWithLabel(category, action, label string, dims ...*dimensions.Values) {
   184  	a.EventWithSourceAndLabel(category, action, a.source, label, dims...)
   185  }
   186  
   187  // EventWithSource should only be used by clients forwarding events on behalf of another source.
   188  // Otherwise, use Event().
   189  func (a *Client) EventWithSource(category, action, source string, dims ...*dimensions.Values) {
   190  	a.EventWithSourceAndLabel(category, action, source, "", dims...)
   191  }
   192  
   193  // EventWithSourceAndLabel should only be used by clients forwarding events on behalf of another
   194  // source (for example, state-svc forwarding events on behalf of State Tool or an executor).
   195  // Otherwise, use EventWithLabel().
   196  func (a *Client) EventWithSourceAndLabel(category, action, source, label string, dims ...*dimensions.Values) {
   197  	if a.customDimensions == nil {
   198  		if condition.InUnitTest() {
   199  			return
   200  		}
   201  		if !condition.BuiltViaCI() {
   202  			panic("Trying to send analytics without configuring the Analytics instance.")
   203  		}
   204  		multilog.Critical("Trying to send analytics event without configuring the Analytics instance.")
   205  		return
   206  	}
   207  
   208  	if a.auth != nil && a.auth.UserID() != nil {
   209  		a.customDimensions.UserID = ptr.To(string(*a.auth.UserID()))
   210  	}
   211  
   212  	a.customDimensions.Sequence = ptr.To(a.sequence)
   213  
   214  	actualDims := mergeDimensions(a.customDimensions, dims...)
   215  
   216  	if a.sequence == *actualDims.Sequence {
   217  		// Increment the sequence number unless dims overrides it (e.g. heartbeats use -1).
   218  		a.sequence++
   219  	}
   220  
   221  	if err := actualDims.PreProcess(); err != nil {
   222  		multilog.Critical("Analytics dimensions cannot be processed properly: %s", errs.JoinMessage(err))
   223  	}
   224  
   225  	a.eventWaitGroup.Add(1)
   226  	// We do not wait for the events to be processed, just scheduling them
   227  	go func() {
   228  		defer a.eventWaitGroup.Done()
   229  		defer func() { handlePanics(recover(), debug.Stack()) }()
   230  		a.report(category, action, source, label, actualDims)
   231  	}()
   232  }
   233  
   234  func handlePanics(err interface{}, stack []byte) {
   235  	if err == nil {
   236  		return
   237  	}
   238  	multilog.Error("Panic in state-svc analytics: %v", err)
   239  	logging.Debug("Stack: %s", string(stack))
   240  }
   241  
   242  func (a *Client) Close() {
   243  	a.Wait()
   244  	a.sendReports = false
   245  }