github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/analytics/analytics_reporter.go (about)

     1  package analytics
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strconv"
     7  	"time"
     8  
     9  	"github.com/tilt-dev/clusterid"
    10  	"github.com/tilt-dev/tilt/internal/analytics"
    11  	"github.com/tilt-dev/tilt/internal/container"
    12  	"github.com/tilt-dev/tilt/internal/controllers/apis/liveupdate"
    13  	"github.com/tilt-dev/tilt/internal/feature"
    14  	"github.com/tilt-dev/tilt/internal/k8s"
    15  	"github.com/tilt-dev/tilt/internal/store"
    16  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    17  )
    18  
    19  // How often to periodically report data for analytics while Tilt is running
    20  const analyticsReportingInterval = time.Minute * 15
    21  
    22  type AnalyticsReporter struct {
    23  	a               *analytics.TiltAnalytics
    24  	store           store.RStore
    25  	kClient         k8s.Client
    26  	env             clusterid.Product
    27  	featureDefaults feature.Defaults
    28  	started         bool
    29  }
    30  
    31  func (ar *AnalyticsReporter) OnChange(ctx context.Context, st store.RStore, _ store.ChangeSummary) error {
    32  	if ar.started {
    33  		return nil
    34  	}
    35  
    36  	state := st.RLockState()
    37  	defer st.RUnlockState()
    38  
    39  	// wait until state has been kinda initialized
    40  	if !state.TiltStartTime.IsZero() && state.LastMainTiltfileError() == nil {
    41  		ar.started = true
    42  		go func() {
    43  			select {
    44  			case <-time.After(10 * time.Second):
    45  				ar.report(ctx) // report once pretty soon after startup...
    46  			case <-ctx.Done():
    47  				return
    48  			}
    49  
    50  			for {
    51  				select {
    52  				case <-time.After(analyticsReportingInterval):
    53  					// and once every <interval> thereafter
    54  					ar.report(ctx)
    55  				case <-ctx.Done():
    56  					return
    57  				}
    58  			}
    59  		}()
    60  	}
    61  
    62  	return nil
    63  }
    64  
    65  var _ store.Subscriber = &AnalyticsReporter{}
    66  
    67  func ProvideAnalyticsReporter(
    68  	a *analytics.TiltAnalytics,
    69  	st store.RStore,
    70  	kClient k8s.Client,
    71  	env clusterid.Product,
    72  	fDefaults feature.Defaults) *AnalyticsReporter {
    73  	return &AnalyticsReporter{
    74  		a:               a,
    75  		store:           st,
    76  		kClient:         kClient,
    77  		env:             env,
    78  		featureDefaults: fDefaults,
    79  		started:         false,
    80  	}
    81  }
    82  
    83  func (ar *AnalyticsReporter) report(ctx context.Context) {
    84  	st := ar.store.RLockState()
    85  	defer ar.store.RUnlockState()
    86  	var dcCount, k8sCount, liveUpdateCount, unbuiltCount,
    87  		sameImgMultiContainerLiveUpdate, multiImgLiveUpdate,
    88  		localCount, localServeCount, enabledCount int
    89  
    90  	labelKeySet := make(map[string]bool)
    91  
    92  	for _, mt := range st.ManifestTargets {
    93  		m := mt.Manifest
    94  		for key := range m.Labels {
    95  			labelKeySet[key] = true
    96  		}
    97  
    98  		if mt.State.DisableState == v1alpha1.DisableStateEnabled {
    99  			enabledCount++
   100  		}
   101  
   102  		if m.IsLocal() {
   103  			localCount++
   104  
   105  			lt := m.LocalTarget()
   106  			if !lt.ServeCmd.Empty() {
   107  				localServeCount++
   108  			}
   109  		}
   110  
   111  		var refInjectCounts map[string]int
   112  		if m.IsK8s() {
   113  			k8sCount++
   114  			refInjectCounts = m.K8sTarget().RefInjectCounts()
   115  			if len(m.ImageTargets) == 0 {
   116  				unbuiltCount++
   117  			}
   118  		}
   119  		if m.IsDC() {
   120  			dcCount++
   121  		}
   122  		var seenLU, multiImgLU, multiContainerLU bool
   123  		for _, it := range m.ImageTargets {
   124  			if !liveupdate.IsEmptySpec(it.LiveUpdateSpec) {
   125  				if !seenLU {
   126  					seenLU = true
   127  					liveUpdateCount++
   128  				} else if !multiImgLU {
   129  					multiImgLU = true
   130  				}
   131  				multiContainerLU = multiContainerLU ||
   132  					refInjectCounts[it.ImageMapSpec.Selector] > 0
   133  			}
   134  		}
   135  		if multiContainerLU {
   136  			sameImgMultiContainerLiveUpdate++
   137  		}
   138  		if multiImgLU {
   139  			multiImgLiveUpdate++
   140  		}
   141  	}
   142  
   143  	stats := map[string]string{
   144  		"up.starttime":           st.TiltStartTime.Format(time.RFC3339),
   145  		"builds.completed_count": strconv.Itoa(st.CompletedBuildCount),
   146  
   147  		// env should really be a global tag, but there's a circular dependency
   148  		// between the global tags and env initialization, so we add it manually.
   149  		"env": k8s.AnalyticsEnv(ar.env),
   150  
   151  		"term_mode": strconv.Itoa(int(st.TerminalMode)),
   152  	}
   153  
   154  	if k8sCount > 1 {
   155  		registry := ar.kClient.LocalRegistry(ctx)
   156  		if !container.IsEmptyRegistry(registry) {
   157  			if registry.Host != "" {
   158  				stats["k8s.registry.host"] = "1"
   159  			}
   160  			if registry.HostFromContainerRuntime != registry.Host {
   161  				stats["k8s.registry.hostFromCluster"] = "1"
   162  			}
   163  		}
   164  
   165  		stats["k8s.runtime"] = string(ar.kClient.ContainerRuntime(ctx))
   166  	}
   167  
   168  	tiltfileIsInError := "false"
   169  	if st.LastMainTiltfileError() != nil {
   170  		tiltfileIsInError = "true"
   171  	} else {
   172  		// only report when there's no tiltfile error, to avoid polluting aggregations
   173  		stats["resource.count"] = strconv.Itoa(len(st.ManifestDefinitionOrder))
   174  		stats["resource.local.count"] = strconv.Itoa(localCount)
   175  		stats["resource.localserve.count"] = strconv.Itoa(localServeCount)
   176  		stats["resource.dockercompose.count"] = strconv.Itoa(dcCount)
   177  		stats["resource.k8s.count"] = strconv.Itoa(k8sCount)
   178  		stats["resource.liveupdate.count"] = strconv.Itoa(liveUpdateCount)
   179  		stats["resource.unbuiltresources.count"] = strconv.Itoa(unbuiltCount)
   180  		stats["resource.sameimagemultiplecontainerliveupdate.count"] = strconv.Itoa(sameImgMultiContainerLiveUpdate)
   181  		stats["resource.multipleimageliveupdate.count"] = strconv.Itoa(multiImgLiveUpdate)
   182  		stats["label.count"] = strconv.Itoa(len(labelKeySet))
   183  		stats["resource.enabled.count"] = strconv.Itoa(enabledCount)
   184  	}
   185  
   186  	stats["tiltfile.error"] = tiltfileIsInError
   187  
   188  	for k, v := range st.Features {
   189  		if ar.featureDefaults[k].Status == feature.Active && v {
   190  			stats[fmt.Sprintf("feature.%s.enabled", k)] = strconv.FormatBool(v)
   191  		}
   192  	}
   193  
   194  	ar.a.Incr("up.running", stats)
   195  }