Skip to content

Commit 0f51657

Browse files
authored
telemetry: improve stack traces, error names, contexts (#844)
Clean up and fix telemetry so that it can be actually useful. This commit is rather large because telemetry happens to touch a lot of different things. Example of what it looks like in Sentry: https://jetpack-io.sentry.io/issues/4047374127 Client ------ - Remove unnecessary telemetry configuration, such as `InitOpts`. Make the `build` package the source of truth for version information and link-time variables. Generate a global execution ID once at startup. - Standardize the contexts to use names and values recognized by Sentry. - Remove arbitrary CLI args and DEVBOX_ environment variables from the context, as these were leaking information. Errors ------ - In combination with the `redact` package, errors can now include data that has been marked as safe for telemetry. - Errors that haven't been marked as safe have a more helpful placeholder containing the chain of error types up until the first exported error. Stack Traces ------------ - Use the correct file and function names. This allows Sentry to group errors correctly. - Don't record every wrapped error. This doesn't make sense in Go since an error is a value containing the entire message. Otherwise you end up with a bunch of error messages that repeat themselves.
1 parent ae5434d commit 0f51657

File tree

12 files changed

+421
-215
lines changed

12 files changed

+421
-215
lines changed

internal/boxcli/featureflag/feature.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,12 @@ func (f *feature) Enabled() bool {
5050
func (f *feature) Disabled() bool {
5151
return !f.Enabled()
5252
}
53+
54+
// All returns a map of all known features flags and whether they're enabled.
55+
func All() map[string]bool {
56+
m := make(map[string]bool, len(features))
57+
for name, feat := range features {
58+
m[name] = feat.Enabled()
59+
}
60+
return m
61+
}

internal/boxcli/midcobra/debug.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ import (
1414
"github.com/spf13/pflag"
1515
"go.jetpack.io/devbox/internal/boxcli/usererr"
1616
"go.jetpack.io/devbox/internal/debug"
17+
"go.jetpack.io/devbox/internal/telemetry"
1718
"go.jetpack.io/devbox/internal/ux"
1819
)
1920

2021
type DebugMiddleware struct {
21-
executionID string // uuid
22-
flag *pflag.Flag
22+
flag *pflag.Flag
2323
}
2424

2525
var _ Middleware = (*DebugMiddleware)(nil)
@@ -69,10 +69,5 @@ func (d *DebugMiddleware) postRun(cmd *cobra.Command, args []string, runErr erro
6969
if errors.As(runErr, &exitErr) {
7070
debug.Log("Command stderr: %s\n", exitErr.Stderr)
7171
}
72-
debug.Log("\nExecutionID:%s\n%+v\n", d.executionID, st)
73-
}
74-
75-
func (d *DebugMiddleware) withExecutionID(execID string) Middleware {
76-
d.executionID = execID
77-
return d
72+
debug.Log("\nExecutionID:%s\n%+v\n", telemetry.ExecutionID, st)
7873
}

internal/boxcli/midcobra/midcobra.go

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ package midcobra
55

66
import (
77
"context"
8-
"encoding/hex"
98
"errors"
109
"os/exec"
1110

12-
"github.com/google/uuid"
1311
"github.com/spf13/cobra"
1412
"go.jetpack.io/devbox/internal/boxcli/usererr"
1513
"go.jetpack.io/devbox/internal/debug"
@@ -24,32 +22,24 @@ type Executable interface {
2422
type Middleware interface {
2523
preRun(cmd *cobra.Command, args []string)
2624
postRun(cmd *cobra.Command, args []string, runErr error)
27-
withExecutionID(execID string) Middleware
2825
}
2926

3027
func New(cmd *cobra.Command) Executable {
3128
return &midcobraExecutable{
3229
cmd: cmd,
33-
executionID: ExecutionID(),
3430
middlewares: []Middleware{},
3531
}
3632
}
3733

3834
type midcobraExecutable struct {
3935
cmd *cobra.Command
4036

41-
// executionID identifies a unique execution of the devbox CLI
42-
executionID string // uuid
43-
4437
middlewares []Middleware
4538
}
4639

4740
var _ Executable = (*midcobraExecutable)(nil)
4841

4942
func (ex *midcobraExecutable) AddMiddleware(mids ...Middleware) {
50-
for index, m := range mids {
51-
mids[index] = m.withExecutionID(ex.executionID)
52-
}
5343
ex.middlewares = append(ex.middlewares, mids...)
5444
}
5545

@@ -101,17 +91,3 @@ func (ex *midcobraExecutable) Execute(ctx context.Context, args []string) int {
10191
}
10292
return 0
10393
}
104-
105-
func ExecutionID() string {
106-
// google/uuid package's String() returns a value of the form:
107-
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
108-
//
109-
// but sentry's EventID specifies:
110-
//
111-
// > EventID is a hexadecimal string representing a unique uuid4 for an Event.
112-
// An EventID must be 32 characters long, lowercase and not have any dashes.
113-
//
114-
// so we pre-process to match sentry's requirements:
115-
id := uuid.New()
116-
return hex.EncodeToString(id[:])
117-
}

internal/boxcli/midcobra/telemetry.go

Lines changed: 44 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ import (
88
"io"
99
"os"
1010
"runtime/trace"
11+
"sort"
12+
"strconv"
1113
"strings"
1214
"time"
1315

14-
"github.com/getsentry/sentry-go"
1516
segment "github.com/segmentio/analytics-go"
1617
"github.com/spf13/cobra"
18+
"github.com/spf13/pflag"
1719
"go.jetpack.io/devbox"
18-
"go.jetpack.io/devbox/internal/boxcli/usererr"
20+
"go.jetpack.io/devbox/internal/boxcli/featureflag"
21+
"go.jetpack.io/devbox/internal/build"
1922
"go.jetpack.io/devbox/internal/cloud/envir"
2023
"go.jetpack.io/devbox/internal/telemetry"
2124
)
@@ -27,60 +30,56 @@ import (
2730
// 2. Data is only stored in SOC 2 compliant systems, and we are SOC 2 compliant ourselves.
2831
// 3. Users should always have the ability to opt-out.
2932
func Telemetry() Middleware {
30-
31-
opts := telemetry.InitOpts()
32-
33-
return &telemetryMiddleware{
34-
opts: *opts,
35-
disabled: telemetry.IsDisabled(opts),
36-
}
33+
return &telemetryMiddleware{}
3734
}
3835

3936
type telemetryMiddleware struct {
40-
// Setup:
41-
opts telemetry.Opts
42-
disabled bool
43-
4437
// Used during execution:
4538
startTime time.Time
46-
47-
executionID string
4839
}
4940

5041
// telemetryMiddleware implements interface Middleware (compile-time check)
5142
var _ Middleware = (*telemetryMiddleware)(nil)
5243

53-
func (m *telemetryMiddleware) withExecutionID(execID string) Middleware {
54-
m.executionID = execID
55-
return m
56-
}
57-
5844
func (m *telemetryMiddleware) preRun(cmd *cobra.Command, args []string) {
5945
m.startTime = telemetry.CommandStartTime()
6046

47+
telemetry.Start(telemetry.AppDevbox)
6148
ctx := cmd.Context()
6249
defer trace.StartRegion(ctx, "telemetryPreRun").End()
63-
if m.disabled {
50+
if !telemetry.Enabled() {
6451
trace.Log(ctx, "telemetry", "telemetry is disabled")
6552
return
6653
}
67-
sentry := telemetry.NewSentry(m.opts.SentryDSN)
68-
sentry.Init(m.opts.AppName, m.opts.AppVersion, m.executionID)
6954
}
7055

7156
func (m *telemetryMiddleware) postRun(cmd *cobra.Command, args []string, runErr error) {
7257
defer trace.StartRegion(cmd.Context(), "telemetryPostRun").End()
73-
if m.disabled {
58+
defer telemetry.Stop()
59+
60+
meta := telemetry.Metadata{
61+
FeatureFlags: featureflag.All(),
62+
CloudRegion: envir.GetRegion(),
63+
CloudCache: os.Getenv("DEVBOX_CACHE"),
64+
}
65+
66+
subcmd, flags, err := getSubcommand(cmd, args)
67+
if err != nil {
68+
// Ignore invalid commands/flags.
7469
return
7570
}
71+
meta.Command = subcmd.CommandPath()
72+
meta.CommandFlags = flags
73+
meta.Packages, meta.NixpkgsHash = getPackagesAndCommitHash(cmd)
74+
meta.InShell, _ = strconv.ParseBool(os.Getenv("DEVBOX_SHELL_ENABLED"))
75+
meta.InBrowser, _ = strconv.ParseBool(os.Getenv("START_WEB_TERMINAL"))
76+
meta.InCloud = envir.IsDevboxCloud()
77+
telemetry.Error(runErr, meta)
7678

7779
evt := m.newEventIfValid(cmd, args, runErr)
7880
if evt == nil {
7981
return
8082
}
81-
82-
m.trackError(evt) // Sentry
83-
8483
m.trackEvent(evt) // Segment
8584
}
8685

@@ -105,7 +104,7 @@ type event struct {
105104
// a valid event.
106105
func (m *telemetryMiddleware) newEventIfValid(cmd *cobra.Command, args []string, runErr error) *event {
107106

108-
subcmd, subargs, parseErr := getSubcommand(cmd, args)
107+
subcmd, flags, parseErr := getSubcommand(cmd, args)
109108
if parseErr != nil {
110109
// Ignore invalid commands
111110
return nil
@@ -126,16 +125,16 @@ func (m *telemetryMiddleware) newEventIfValid(cmd *cobra.Command, args []string,
126125

127126
return &event{
128127
Event: telemetry.Event{
129-
AnonymousID: telemetry.DeviceID(),
130-
AppName: m.opts.AppName,
131-
AppVersion: m.opts.AppVersion,
128+
AnonymousID: telemetry.DeviceID,
129+
AppName: telemetry.AppDevbox,
130+
AppVersion: build.Version,
132131
CloudRegion: envir.GetRegion(),
133132
Duration: time.Since(m.startTime),
134-
OsName: telemetry.OS(),
133+
OsName: build.OS(),
135134
UserID: userID,
136135
},
137136
Command: subcmd.CommandPath(),
138-
CommandArgs: subargs,
137+
CommandArgs: flags,
139138
CommandError: runErr,
140139
// The command is hidden if either the top-level command is hidden or
141140
// the specific sub-command that was executed is hidden.
@@ -149,38 +148,15 @@ func (m *telemetryMiddleware) newEventIfValid(cmd *cobra.Command, args []string,
149148
}
150149
}
151150

152-
func (m *telemetryMiddleware) trackError(evt *event) {
153-
// Ensure error is not nil and not a non-loggable user error
154-
if evt == nil || !usererr.ShouldLogError(evt.CommandError) {
155-
return
156-
}
157-
158-
sentry.ConfigureScope(func(scope *sentry.Scope) {
159-
scope.SetTag("command", evt.Command)
160-
scope.SetContext("command", map[string]interface{}{
161-
"command": evt.Command,
162-
"command args": evt.CommandArgs,
163-
"packages": evt.Packages,
164-
"nixpkgs hash": evt.CommitHash,
165-
"in shell": evt.InDevboxShell,
166-
})
167-
scope.SetContext("devbox env", evt.DevboxEnv)
168-
})
169-
sentry.CaptureException(evt.CommandError)
170-
}
171-
172151
func (m *telemetryMiddleware) trackEvent(evt *event) {
173152
if evt == nil || evt.CommandHidden {
174153
return
175154
}
176155

177156
if evt.CommandError != nil {
178-
// verified with manual testing that the sentryID returned by CaptureException
179-
// is the same as m.ExecutionID, since we set EventID = m.ExecutionID in sentry.Init
180-
evt.SentryEventID = m.executionID
157+
evt.SentryEventID = telemetry.ExecutionID
181158
}
182-
183-
segmentClient := telemetry.NewSegmentClient(m.opts.TelemetryKey)
159+
segmentClient := telemetry.NewSegmentClient(build.TelemetryKey)
184160
defer func() {
185161
_ = segmentClient.Close()
186162
}()
@@ -219,13 +195,18 @@ func (m *telemetryMiddleware) trackEvent(evt *event) {
219195
})
220196
}
221197

222-
func getSubcommand(c *cobra.Command, args []string) (subcmd *cobra.Command, subargs []string, err error) {
223-
if c.TraverseChildren {
224-
subcmd, subargs, err = c.Traverse(args)
198+
func getSubcommand(cmd *cobra.Command, args []string) (subcmd *cobra.Command, flags []string, err error) {
199+
if cmd.TraverseChildren {
200+
subcmd, _, err = cmd.Traverse(args)
225201
} else {
226-
subcmd, subargs, err = c.Find(args)
202+
subcmd, _, err = cmd.Find(args)
227203
}
228-
return subcmd, subargs, err
204+
205+
subcmd.Flags().Visit(func(f *pflag.Flag) {
206+
flags = append(flags, "--"+f.Name)
207+
})
208+
sort.Strings(flags)
209+
return subcmd, flags, err
229210
}
230211

231212
func getPackagesAndCommitHash(c *cobra.Command) ([]string, string) {

internal/boxcli/midcobra/trace.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@ import (
1313
)
1414

1515
type TraceMiddleware struct {
16-
executionID string // uuid
17-
tracef *os.File
18-
flag *pflag.Flag
19-
task *trace.Task
16+
tracef *os.File
17+
flag *pflag.Flag
18+
task *trace.Task
2019
}
2120

2221
var _ Middleware = (*DebugMiddleware)(nil)
@@ -28,7 +27,7 @@ func (t *TraceMiddleware) AttachToFlag(flags *pflag.FlagSet, flagName string) {
2827
t.flag.NoOptDefVal = "trace.out"
2928
}
3029

31-
func (t *TraceMiddleware) preRun(cmd *cobra.Command, args []string) {
30+
func (t *TraceMiddleware) preRun(cmd *cobra.Command, _ []string) {
3231
if t == nil {
3332
return
3433
}
@@ -50,7 +49,7 @@ func (t *TraceMiddleware) preRun(cmd *cobra.Command, args []string) {
5049
cmd.SetContext(ctx)
5150
}
5251

53-
func (t *TraceMiddleware) postRun(cmd *cobra.Command, args []string, runErr error) {
52+
func (t *TraceMiddleware) postRun(*cobra.Command, []string, error) {
5453
if t.tracef == nil {
5554
return
5655
}
@@ -60,8 +59,3 @@ func (t *TraceMiddleware) postRun(cmd *cobra.Command, args []string, runErr erro
6059
panic("error closing trace file: " + err.Error())
6160
}
6261
}
63-
64-
func (t *TraceMiddleware) withExecutionID(execID string) Middleware {
65-
t.executionID = execID
66-
return t
67-
}

internal/boxcli/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func Execute(ctx context.Context, args []string) int {
8181
func Main() {
8282
if strings.HasSuffix(os.Args[0], "ssh") ||
8383
strings.HasSuffix(os.Args[0], "scp") {
84-
code := sshshim.Execute(context.Background(), os.Args)
84+
code := sshshim.Execute(os.Args)
8585
os.Exit(code)
8686
}
8787
code := Execute(context.Background(), os.Args[1:])

internal/build/build.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,48 @@
11
package build
22

3+
import (
4+
"runtime"
5+
"sync"
6+
7+
"go.jetpack.io/devbox/internal/fileutil"
8+
)
9+
310
// Variables in this file are set via ldflags.
411
var (
12+
IsDev = Version == "0.0.0-dev"
513
Version = "0.0.0-dev"
614
Commit = "none"
715
CommitDate = "unknown"
816

917
SentryDSN = "" // Disabled by default
1018
TelemetryKey = "" // Disabled by default
1119
)
20+
21+
// User-presentable names of operating systems supported by Devbox.
22+
const (
23+
OSLinux = "Linux"
24+
OSDarwin = "macOS"
25+
OSWSL = "WSL"
26+
)
27+
28+
var (
29+
osName string
30+
osOnce sync.Once
31+
)
32+
33+
func OS() string {
34+
osOnce.Do(func() {
35+
switch runtime.GOOS {
36+
case "linux":
37+
if fileutil.Exists("/proc/sys/fs/binfmt_misc/WSLInterop") || fileutil.Exists("/run/WSL") {
38+
osName = OSWSL
39+
}
40+
osName = OSLinux
41+
case "darwin":
42+
osName = OSDarwin
43+
default:
44+
osName = runtime.GOOS
45+
}
46+
})
47+
return osName
48+
}

0 commit comments

Comments
 (0)