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 }