1 package segment
2
3 import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "net"
9 "os"
10 "os/user"
11 "path/filepath"
12 "regexp"
13 "runtime"
14 "strings"
15 "time"
16
17 "github.com/Xuanwo/go-locale"
18
19 "github.com/redhat-developer/odo/pkg/config"
20 scontext "github.com/redhat-developer/odo/pkg/segment/context"
21
22 "github.com/google/uuid"
23 "golang.org/x/term"
24 "gopkg.in/segmentio/analytics-go.v3"
25 "k8s.io/klog"
26 "k8s.io/utils/pointer"
27
28 "github.com/redhat-developer/odo/pkg/preference"
29 )
30
31
32 var writeKey = "4xGV1HV7K2FtUWaoAozSBD7SNCBCJ65U"
33
34
35 const Sanitizer = "XXXX"
36
37 const TelemetryClient = "odo"
38
39
40
41 const (
42
43
44
45
46
47 DisableTelemetryEnv = "ODO_DISABLE_TELEMETRY"
48
49
50
51
52 TrackingConsentEnv = "ODO_TRACKING_CONSENT"
53 )
54
55 type TelemetryProperties struct {
56 Duration int64 `json:"duration"`
57 Error string `json:"error"`
58 ErrorType string `json:"errortype"`
59 Success bool `json:"success"`
60 Tty bool `json:"tty"`
61 Version string `json:"version"`
62 CmdProperties map[string]interface{} `json:"cmdProperties"`
63 }
64
65 type TelemetryData struct {
66 Event string `json:"event"`
67 Properties TelemetryProperties `json:"properties"`
68 }
69
70 type Client struct {
71
72 SegmentClient analytics.Client
73
74 TelemetryFilePath string
75 }
76
77 func GetApikey() string {
78 return writeKey
79 }
80
81
82 func NewClient() (*Client, error) {
83 return newCustomClient(
84 GetTelemetryFilePath(),
85 analytics.DefaultEndpoint,
86 )
87 }
88
89
90 func newCustomClient(telemetryFilePath string, segmentEndpoint string) (*Client, error) {
91
92 tag, err := locale.Detect()
93 if err != nil {
94 klog.V(4).Infof("couldn't fetch locale info: %s", err.Error())
95 }
96
97 client, err := analytics.NewWithConfig(writeKey, analytics.Config{
98 Endpoint: segmentEndpoint,
99 Verbose: true,
100 DefaultContext: &analytics.Context{
101 IP: net.IPv4(0, 0, 0, 0),
102 Timezone: getTimeZoneRelativeToUTC(),
103 OS: analytics.OSInfo{
104 Name: runtime.GOOS,
105 },
106 Locale: tag.String(),
107 },
108 })
109 if err != nil {
110 return nil, err
111 }
112 return &Client{
113 SegmentClient: client,
114 TelemetryFilePath: telemetryFilePath,
115 }, nil
116 }
117
118
119 func getTimeZoneRelativeToUTC() string {
120
121
122 var t = strings.Split(time.Now().Format(time.RFC822Z), " ")
123 return fmt.Sprintf("UTC %s", t[len(t)-1])
124 }
125
126
127 func (c *Client) Close() error {
128 return c.SegmentClient.Close()
129 }
130
131
132 func (c *Client) Upload(ctx context.Context, data TelemetryData) error {
133
134 if !scontext.GetTelemetryStatus(ctx) {
135 return nil
136 }
137
138
139 userId, uerr := GetUserIdentity(c.TelemetryFilePath)
140 if uerr != nil {
141 return uerr
142 }
143
144
145 properties := analytics.NewProperties()
146 for k, v := range data.Properties.CmdProperties {
147 if k != scontext.TelemetryStatus {
148 properties = properties.Set(k, v)
149 }
150 }
151
152 properties = properties.Set("version", data.Properties.Version).
153 Set("success", data.Properties.Success).
154 Set("duration(ms)", data.Properties.Duration).
155 Set("tty", data.Properties.Tty)
156
157 if data.Properties.Error != "" {
158 properties = properties.Set("error", data.Properties.Error).Set("error-type", data.Properties.ErrorType)
159 }
160
161
162 err := c.SegmentClient.Enqueue(analytics.Identify{
163 UserId: userId,
164 Traits: addConfigTraits(),
165 })
166 if err != nil {
167 klog.V(4).Infof("Cannot send Identify telemetry event: %q", err)
168
169
170
171 }
172
173
174 return c.SegmentClient.Enqueue(analytics.Track{
175 UserId: userId,
176 Event: data.Event,
177 Properties: properties,
178 })
179 }
180
181
182 func addConfigTraits() analytics.Traits {
183 traits := analytics.NewTraits().Set("os", runtime.GOOS)
184 traits.Set("timezone", getTimeZoneRelativeToUTC())
185
186 tag, err := locale.Detect()
187 if err != nil {
188 klog.V(4).Infof("couldn't fetch locale info: %s", err.Error())
189 } else {
190 traits.Set("locale", tag.String())
191 }
192 return traits
193 }
194
195
196 func GetTelemetryFilePath() string {
197 homeDir, _ := os.UserHomeDir()
198 return filepath.Join(homeDir, ".redhat", "anonymousId")
199 }
200
201
202 func GetUserIdentity(telemetryFilePath string) (string, error) {
203 var id []byte
204
205
206 if err := os.MkdirAll(filepath.Dir(telemetryFilePath), os.ModePerm); err != nil {
207 return "", err
208 }
209
210
211 if _, err := os.Stat(telemetryFilePath); !os.IsNotExist(err) {
212 id, err = os.ReadFile(telemetryFilePath)
213 if err != nil {
214 return "", err
215 }
216 }
217
218
219 if _, err := uuid.ParseBytes(bytes.TrimSpace(id)); err != nil {
220 u, uErr := uuid.NewRandom()
221 if uErr != nil {
222 return "", fmt.Errorf("failed to generate anonymous ID for telemetry: %w", uErr)
223 }
224 id = []byte(u.String())
225 if err := os.WriteFile(telemetryFilePath, id, 0o600); err != nil {
226 return "", err
227 }
228 }
229 return string(bytes.TrimSpace(id)), nil
230 }
231
232
233 func SetError(err error) (errString string) {
234 if err == nil {
235 return ""
236 }
237 errString = err.Error()
238
239
240 errString = sanitizeUserInfo(errString)
241
242
243 errString = sanitizeFilePath(errString)
244
245
246 errString = sanitizeExec(errString)
247
248
249 errString = sanitizeURL(errString)
250
251 return errString
252 }
253
254
255 func ErrorType(err error) string {
256 if err == nil {
257 return ""
258 }
259 wrappedErr := errors.Unwrap(err)
260 if wrappedErr != nil {
261 return fmt.Sprintf("%T", wrappedErr)
262 }
263 return fmt.Sprintf("%T", err)
264 }
265
266
267 func RunningInTerminal() bool {
268 return term.IsTerminal(int(os.Stdin.Fd()))
269 }
270
271
272 func IsTelemetryEnabled(cfg preference.Client, envConfig config.Configuration) bool {
273 klog.V(4).Info("Checking telemetry enable status")
274
275
276
277
278 disableTelemetry := pointer.BoolDeref(envConfig.OdoDisableTelemetry, false)
279 if disableTelemetry {
280
281 klog.V(4).Infof("Sending telemetry disabled by %q env variable\n", DisableTelemetryEnv)
282 return false
283 }
284
285 _, trackingConsentEnabled, present, err := IsTrackingConsentEnabled(&envConfig)
286 if err != nil {
287 klog.V(4).Infof("error in determining value of tracking consent env var: %v", err)
288 } else if present {
289
290 if !trackingConsentEnabled {
291 klog.V(4).Info("Sending telemetry disabled by env variable\n")
292 return false
293 }
294 klog.V(4).Info("Sending telemetry enabled by env variable\n")
295 return true
296 }
297
298 isEnabled := cfg.GetConsentTelemetry()
299 s := "Sending telemetry disabled by preference"
300 if isEnabled {
301 s = "Sending telemetry enabled by preference"
302 }
303 klog.V(4).Infof("%s\n", s)
304 return isEnabled
305 }
306
307
308
309 func IsTrackingConsentEnabled(envConfig *config.Configuration) (value string, enabled bool, present bool, err error) {
310 if envConfig.OdoTrackingConsent == nil {
311 return "", false, false, nil
312 }
313 trackingConsent := *envConfig.OdoTrackingConsent
314 switch trackingConsent {
315 case "yes":
316 return trackingConsent, true, true, nil
317 case "no":
318 return trackingConsent, false, true, nil
319 default:
320 return trackingConsent, false, true, fmt.Errorf("invalid value for %s: %q", TrackingConsentEnv, trackingConsent)
321 }
322 }
323
324
325 func sanitizeUserInfo(errString string) string {
326 user1, err1 := user.Current()
327 if err1 != nil {
328 return err1.Error()
329 }
330 errString = strings.ReplaceAll(errString, user1.Username, Sanitizer)
331 return errString
332 }
333
334
335 func sanitizeFilePath(errString string) string {
336 for _, str := range strings.Split(errString, " ") {
337 if strings.Count(str, string(os.PathSeparator)) > 1 {
338 errString = strings.ReplaceAll(errString, str, Sanitizer)
339 }
340 }
341 return errString
342 }
343
344
345 func sanitizeURL(errString string) string {
346
347
348
349 urlPattern, err := regexp.Compile(`((https?|ftp|smtp)://)?((?:[0-9]{1,3}\.){3}[0-9]{1,3}(:([0-9]{1,5}))?|([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,})`)
350 if err != nil {
351 return errString
352 }
353 errString = urlPattern.ReplaceAllString(errString, Sanitizer)
354 return errString
355 }
356
357
358 func sanitizeExec(errString string) string {
359 pattern, _ := regexp.Compile("exec command.*")
360 errString = pattern.ReplaceAllString(errString, fmt.Sprintf("exec command %s", Sanitizer))
361 return errString
362 }
363
View as plain text