github.com/drud/ddev@v1.21.5-alpha1.0.20230226034409-94fcc4b94453/pkg/ddevapp/instrumentation.go (about)

     1  package ddevapp
     2  
     3  import (
     4  	"crypto/hmac"
     5  	"crypto/sha256"
     6  	"encoding/hex"
     7  	"fmt"
     8  	"github.com/denisbrodbeck/machineid"
     9  	"github.com/drud/ddev/pkg/dockerutil"
    10  	"github.com/drud/ddev/pkg/globalconfig"
    11  	"github.com/drud/ddev/pkg/nodeps"
    12  	"github.com/drud/ddev/pkg/output"
    13  	"github.com/drud/ddev/pkg/util"
    14  	"github.com/drud/ddev/pkg/version"
    15  	"github.com/drud/ddev/pkg/versionconstants"
    16  	"gopkg.in/segmentio/analytics-go.v3"
    17  	"os"
    18  	"runtime"
    19  	"strconv"
    20  	"strings"
    21  	"time"
    22  )
    23  
    24  var hashedHostID string
    25  
    26  // SegmentNoopLogger defines a no-op logger to prevent Segment log messages from being emitted
    27  type SegmentNoopLogger struct{}
    28  
    29  func (n *SegmentNoopLogger) Logf(format string, args ...interface{})   {}
    30  func (n *SegmentNoopLogger) Errorf(format string, args ...interface{}) {}
    31  
    32  // ReportableEvents is the list of events that we choose to report specifically.
    33  // Excludes non-ddev custom commands.
    34  var ReportableEvents = map[string]bool{"start": true}
    35  
    36  // GetInstrumentationUser normally gets just the hashed hostID but if
    37  // an explicit user is provided in global_config.yaml that will be prepended.
    38  func GetInstrumentationUser() string {
    39  	return hashedHostID
    40  }
    41  
    42  // SetInstrumentationBaseTags sets the basic always-used tags for Segment
    43  func SetInstrumentationBaseTags() {
    44  	runTime := util.TimeTrack(time.Now(), "SetInstrumentationBaseTags")
    45  	defer runTime()
    46  
    47  	if globalconfig.DdevGlobalConfig.InstrumentationOptIn {
    48  		dockerVersion, _ := dockerutil.GetDockerVersion()
    49  		dockerPlaform, _ := version.GetDockerPlatform()
    50  		timezone, _ := time.Now().In(time.Local).Zone()
    51  		lang := os.Getenv("LANG")
    52  
    53  		nodeps.InstrumentationTags["OS"] = runtime.GOOS
    54  		nodeps.InstrumentationTags["architecture"] = runtime.GOARCH
    55  		wslDistro := nodeps.GetWSLDistro()
    56  		if wslDistro != "" {
    57  			nodeps.InstrumentationTags["isWSL"] = "true"
    58  			nodeps.InstrumentationTags["wslDistro"] = wslDistro
    59  			nodeps.InstrumentationTags["OS"] = "wsl2"
    60  		}
    61  		nodeps.InstrumentationTags["dockerVersion"] = dockerVersion
    62  		nodeps.InstrumentationTags["dockerPlatform"] = dockerPlaform
    63  		nodeps.InstrumentationTags["dockerToolbox"] = strconv.FormatBool(false)
    64  		nodeps.InstrumentationTags["version"] = versionconstants.DdevVersion
    65  		nodeps.InstrumentationTags["ServerHash"] = GetInstrumentationUser()
    66  		nodeps.InstrumentationTags["timezone"] = timezone
    67  		nodeps.InstrumentationTags["language"] = lang
    68  	}
    69  }
    70  
    71  // getProjectHash combines the machine ID and project name and then
    72  // hashes the result, so we can end up with a unique project id
    73  func getProjectHash(projectName string) string {
    74  	ph := hmac.New(sha256.New, []byte(GetInstrumentationUser()+projectName))
    75  	_, _ = ph.Write([]byte("phash"))
    76  	return hex.EncodeToString(ph.Sum(nil))
    77  }
    78  
    79  // SetInstrumentationAppTags creates app-specific tags for Segment
    80  func (app *DdevApp) SetInstrumentationAppTags() {
    81  	runTime := util.TimeTrack(time.Now(), "SetInstrumentationAppTags")
    82  	defer runTime()
    83  
    84  	ignoredProperties := []string{"approot", "hostname", "hostnames", "name", "router_status_log", "shortroot"}
    85  
    86  	describeTags, _ := app.Describe(false)
    87  	for key, val := range describeTags {
    88  		// Make sure none of the "URL" attributes or the ignoredProperties comes through
    89  		if strings.Contains(strings.ToLower(key), "url") || nodeps.ArrayContainsString(ignoredProperties, key) {
    90  			continue
    91  		}
    92  		nodeps.InstrumentationTags[key] = fmt.Sprintf("%v", val)
    93  	}
    94  	nodeps.InstrumentationTags["ProjectID"] = getProjectHash(app.Name)
    95  }
    96  
    97  // SegmentUser does the enqueue of the Identify action, identifying the user
    98  // Here we just use the hashed hostid as the user id
    99  func SegmentUser(client analytics.Client, hashedID string) error {
   100  	timezone, _ := time.Now().In(time.Local).Zone()
   101  	lang := os.Getenv("LANG")
   102  	err := client.Enqueue(analytics.Identify{
   103  		UserId:  hashedID,
   104  		Context: &analytics.Context{App: analytics.AppInfo{Name: "ddev", Version: versionconstants.DdevVersion}, OS: analytics.OSInfo{Name: runtime.GOOS}, Locale: lang, Timezone: timezone},
   105  		Traits:  analytics.Traits{"instrumentation_user": globalconfig.DdevGlobalConfig.InstrumentationUser},
   106  	})
   107  
   108  	if err != nil {
   109  		return err
   110  	}
   111  	return nil
   112  }
   113  
   114  // SegmentEvent provides the event and traits that go with it.
   115  func SegmentEvent(client analytics.Client, hashedID string, event string) error {
   116  	if _, ok := ReportableEvents[event]; !ok {
   117  		// There's no need to waste people's time on custom commands.
   118  		return nil
   119  	}
   120  	properties := analytics.NewProperties()
   121  
   122  	for key, val := range nodeps.InstrumentationTags {
   123  		if val != "" {
   124  			properties = properties.Set(key, val)
   125  		}
   126  	}
   127  	timezone, _ := time.Now().In(time.Local).Zone()
   128  	lang := os.Getenv("LANG")
   129  	err := client.Enqueue(analytics.Track{
   130  		UserId:     hashedID,
   131  		Event:      event,
   132  		Properties: properties,
   133  		Context:    &analytics.Context{App: analytics.AppInfo{Name: "ddev", Version: versionconstants.DdevVersion}, OS: analytics.OSInfo{Name: runtime.GOOS}, Locale: lang, Timezone: timezone},
   134  	})
   135  
   136  	return err
   137  }
   138  
   139  // SendInstrumentationEvents does the actual send to segment
   140  func SendInstrumentationEvents(event string) {
   141  	runTime := util.TimeTrack(time.Now(), "SendInstrumentationEvents")
   142  	defer runTime()
   143  
   144  	if globalconfig.DdevGlobalConfig.InstrumentationOptIn && globalconfig.IsInternetActive() {
   145  		client, _ := analytics.NewWithConfig(versionconstants.SegmentKey, analytics.Config{
   146  			Logger: &SegmentNoopLogger{},
   147  		})
   148  
   149  		err := SegmentEvent(client, GetInstrumentationUser(), event)
   150  		if err != nil {
   151  			output.UserOut.Debugf("error sending event to segment: %v", err)
   152  		}
   153  		err = client.Close()
   154  		if err != nil {
   155  			output.UserOut.Debugf("segment analytics client.close() failed: %v", err)
   156  		}
   157  	}
   158  }
   159  
   160  func init() {
   161  	hashedHostID, _ = machineid.ProtectedID("ddev")
   162  }