Skip to content

Commit 80a444d

Browse files
authored
telemetry: report segment events asynchronously (#1222)
Instead of blocking the process from exiting, buffer Segment events to disk and start another process to upload them. With this change all telemetry should be non-blocking. Most of the Segment event logic now lives in the telemetry package instead of the cobra middleware. A bunch of the unused logic around tracking CLI SSH sessions has also been removed. The telemetry package API now looks something like: ```go type EventName int const ( EventCommandSuccess EventName = iota EventShellInteractive EventShellReady ) func Error(err error, meta Metadata) func Event(e EventName, meta Metadata) func Start() func Stop() func Upload() ``` Fixes #1003.
1 parent 166cfcf commit 80a444d

File tree

12 files changed

+426
-515
lines changed

12 files changed

+426
-515
lines changed

internal/boxcli/log.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,21 @@ func doLogCommand(cmd *cobra.Command, args []string) error {
2727
return usererr.New("expect an <event-name> arg for command: %s", cmd.CommandPath())
2828
}
2929

30-
if args[0] == "shell-ready" || args[0] == "shell-interactive" {
30+
switch eventName := args[0]; eventName {
31+
case "shell-ready":
3132
if len(args) < 2 {
3233
return usererr.New("expected a start-time argument for logging the shell-ready event")
3334
}
34-
return telemetry.LogShellDurationEvent(args[0] /*event name*/, args[1] /*startTime*/)
35+
telemetry.Event(telemetry.EventShellReady, telemetry.Metadata{
36+
CommandStart: telemetry.ParseShellStart(args[1]),
37+
})
38+
case "shell-interactive":
39+
if len(args) < 2 {
40+
return usererr.New("expected a start-time argument for logging the shell-interactive event")
41+
}
42+
telemetry.Event(telemetry.EventShellInteractive, telemetry.Metadata{
43+
CommandStart: telemetry.ParseShellStart(args[1]),
44+
})
3545
}
3646
return usererr.New("unrecognized event-name %s for command: %s", args[0], cmd.CommandPath())
3747
}

internal/boxcli/midcobra/telemetry.go

Lines changed: 5 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,15 @@
44
package midcobra
55

66
import (
7-
"fmt"
87
"os"
98
"runtime/trace"
109
"sort"
11-
"strings"
12-
"time"
1310

14-
segment "github.com/segmentio/analytics-go"
1511
"github.com/spf13/cobra"
1612
"github.com/spf13/pflag"
1713

1814
"go.jetpack.io/devbox"
1915
"go.jetpack.io/devbox/internal/boxcli/featureflag"
20-
"go.jetpack.io/devbox/internal/build"
2116
"go.jetpack.io/devbox/internal/envir"
2217
"go.jetpack.io/devbox/internal/impl/devopt"
2318
"go.jetpack.io/devbox/internal/telemetry"
@@ -33,24 +28,13 @@ func Telemetry() Middleware {
3328
return &telemetryMiddleware{}
3429
}
3530

36-
type telemetryMiddleware struct {
37-
// Used during execution:
38-
startTime time.Time
39-
}
31+
type telemetryMiddleware struct{}
4032

4133
// telemetryMiddleware implements interface Middleware (compile-time check)
4234
var _ Middleware = (*telemetryMiddleware)(nil)
4335

4436
func (m *telemetryMiddleware) preRun(cmd *cobra.Command, args []string) {
45-
m.startTime = telemetry.CommandStartTime()
46-
47-
telemetry.Start(telemetry.AppDevbox)
48-
ctx := cmd.Context()
49-
defer trace.StartRegion(ctx, "telemetryPreRun").End()
50-
if !telemetry.Enabled() {
51-
trace.Log(ctx, "telemetry", "telemetry is disabled")
52-
return
53-
}
37+
telemetry.Start()
5438
}
5539

5640
func (m *telemetryMiddleware) postRun(cmd *cobra.Command, args []string, runErr error) {
@@ -74,127 +58,12 @@ func (m *telemetryMiddleware) postRun(cmd *cobra.Command, args []string, runErr
7458
meta.InShell = envir.IsDevboxShellEnabled()
7559
meta.InBrowser = envir.IsInBrowser()
7660
meta.InCloud = envir.IsDevboxCloud()
77-
telemetry.Error(runErr, meta)
78-
79-
if !telemetry.Enabled() {
80-
return
81-
}
82-
evt := m.newEventIfValid(cmd, args, runErr)
83-
if evt == nil {
84-
return
85-
}
86-
m.trackEvent(evt) // Segment
87-
}
88-
89-
// Consider renaming this to commandEvent
90-
// since it has info about the specific command run.
91-
type event struct {
92-
telemetry.Event
93-
Command string
94-
CommandArgs []string
95-
CommandError error
96-
CommandHidden bool
97-
Failed bool
98-
Packages []string
99-
CommitHash string // the nikpkgs commit hash in devbox.json
100-
InDevboxShell bool
101-
DevboxEnv map[string]any // Devbox-specific environment variables
102-
SentryEventID string
103-
Shell string
104-
}
105-
106-
// newEventIfValid creates a new telemetry event, but returns nil if we cannot construct
107-
// a valid event.
108-
func (m *telemetryMiddleware) newEventIfValid(cmd *cobra.Command, args []string, runErr error) *event {
109-
subcmd, flags, parseErr := getSubcommand(cmd, args)
110-
if parseErr != nil {
111-
// Ignore invalid commands
112-
return nil
113-
}
114-
115-
pkgs, hash := getPackagesAndCommitHash(cmd)
11661

117-
// an empty userID means that we do not have a github username saved
118-
userID := telemetry.UserIDFromGithubUsername()
119-
120-
devboxEnv := map[string]interface{}{}
121-
for _, e := range os.Environ() {
122-
if strings.HasPrefix(e, "DEVBOX") && strings.Contains(e, "=") {
123-
key := strings.Split(e, "=")[0]
124-
devboxEnv[key] = os.Getenv(key)
125-
}
126-
}
127-
128-
return &event{
129-
Event: telemetry.Event{
130-
AnonymousID: telemetry.DeviceID,
131-
AppName: telemetry.AppDevbox,
132-
AppVersion: build.Version,
133-
CloudRegion: os.Getenv(envir.DevboxRegion),
134-
Duration: time.Since(m.startTime),
135-
OsName: build.OS(),
136-
UserID: userID,
137-
},
138-
Command: subcmd.CommandPath(),
139-
CommandArgs: flags,
140-
CommandError: runErr,
141-
// The command is hidden if either the top-level command is hidden or
142-
// the specific sub-command that was executed is hidden.
143-
CommandHidden: cmd.Hidden || subcmd.Hidden,
144-
Failed: runErr != nil,
145-
Packages: pkgs,
146-
CommitHash: hash,
147-
InDevboxShell: envir.IsDevboxShellEnabled(),
148-
DevboxEnv: devboxEnv,
149-
Shell: os.Getenv(envir.Shell),
150-
}
151-
}
152-
153-
func (m *telemetryMiddleware) trackEvent(evt *event) {
154-
if evt == nil || evt.CommandHidden {
62+
if runErr != nil {
63+
telemetry.Error(runErr, meta)
15564
return
15665
}
157-
158-
if evt.CommandError != nil {
159-
evt.SentryEventID = telemetry.ExecutionID
160-
}
161-
segmentClient := telemetry.NewSegmentClient(build.TelemetryKey)
162-
defer func() {
163-
_ = segmentClient.Close()
164-
}()
165-
166-
// deliberately ignore error
167-
_ = segmentClient.Enqueue(segment.Identify{
168-
AnonymousId: evt.AnonymousID,
169-
UserId: evt.UserID,
170-
})
171-
172-
_ = segmentClient.Enqueue(segment.Track{ // Ignore errors, telemetry is best effort
173-
AnonymousId: evt.AnonymousID, // Use device id instead
174-
Event: fmt.Sprintf("[%s] Command: %s", evt.AppName, evt.Command),
175-
Context: &segment.Context{
176-
Device: segment.DeviceInfo{
177-
Id: evt.AnonymousID,
178-
},
179-
App: segment.AppInfo{
180-
Name: evt.AppName,
181-
Version: evt.AppVersion,
182-
},
183-
OS: segment.OSInfo{
184-
Name: evt.OsName,
185-
},
186-
},
187-
Properties: segment.NewProperties().
188-
Set("cloud_region", evt.CloudRegion).
189-
Set("command", evt.Command).
190-
Set("command_args", evt.CommandArgs).
191-
Set("failed", evt.Failed).
192-
Set("duration", evt.Duration.Milliseconds()).
193-
Set("packages", evt.Packages).
194-
Set("sentry_event_id", evt.SentryEventID).
195-
Set("shell", evt.Shell),
196-
UserId: evt.UserID,
197-
})
66+
telemetry.Event(telemetry.EventCommandSuccess, meta)
19867
}
19968

20069
func getSubcommand(cmd *cobra.Command, args []string) (subcmd *cobra.Command, flags []string, err error) {

internal/boxcli/root.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"io"
1010
"os"
1111
"strings"
12+
"time"
1213

1314
"github.com/spf13/cobra"
1415

@@ -110,8 +111,14 @@ func Main() {
110111
os.Exit(sshshim.Execute(ctx, os.Args))
111112
}
112113

113-
if len(os.Args) > 1 && os.Args[1] == "bug" {
114-
telemetry.ReportErrors()
114+
if len(os.Args) > 1 && os.Args[1] == "upload-telemetry" {
115+
// This subcommand is hidden and only run by devbox itself as a
116+
// child process. We need to really make sure that we always
117+
// exit and don't leave orphaned processes laying around.
118+
time.AfterFunc(5*time.Second, func() {
119+
os.Exit(0)
120+
})
121+
telemetry.Upload()
115122
return
116123
}
117124

internal/cloud/cloud.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ func shell(username, hostname, projectDir string, shellStartTime time.Time) erro
445445
cmd := &openssh.Cmd{
446446
DestinationAddr: hostname,
447447
PathInVM: absoluteProjectPathInVM(username, projectPath),
448-
ShellStartTime: telemetry.UnixTimestampFromTime(shellStartTime),
448+
ShellStartTime: telemetry.FormatShellStart(shellStartTime),
449449
Username: username,
450450
}
451451
sessionErrors := newSSHSessionErrors()

internal/cloud/openssh/sshshim/command.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414

1515
func Execute(ctx context.Context, args []string) int {
1616
defer debug.Recover()
17-
telemetry.Start(telemetry.AppSSHShim)
17+
telemetry.Start()
1818
defer telemetry.Stop()
1919

2020
if err := execute(ctx, args); err != nil {

internal/envir/env.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package envir
55

66
const (
77
DevboxCache = "DEVBOX_CACHE"
8-
devboxCLICloudShell = "DEVBOX_CLI_CLOUD_SHELL"
98
DevboxDebug = "DEVBOX_DEBUG"
109
DevboxFeaturePrefix = "DEVBOX_FEATURE_"
1110
DevboxGateway = "DEVBOX_GATEWAY"
@@ -21,7 +20,8 @@ const (
2120
LauncherVersion = "LAUNCHER_VERSION"
2221
LauncherPath = "LAUNCHER_PATH"
2322

24-
SSHTTY = "SSH_TTY"
23+
GitHubUsername = "GITHUB_USER_NAME"
24+
SSHTTY = "SSH_TTY"
2525

2626
XDGDataHome = "XDG_DATA_HOME"
2727
XDGConfigHome = "XDG_CONFIG_HOME"

internal/envir/util.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@ import (
88
"strconv"
99
)
1010

11-
func IsCLICloudShell() bool { // TODO: not used any more
12-
cliCloudShell, _ := strconv.ParseBool(os.Getenv(devboxCLICloudShell))
13-
return cliCloudShell
14-
}
15-
1611
func IsDevboxCloud() bool {
1712
return os.Getenv(DevboxRegion) != ""
1813
}

internal/impl/devbox.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"go.jetpack.io/devbox/internal/boxcli/featureflag"
2323
"go.jetpack.io/devbox/internal/impl/generate"
2424
"go.jetpack.io/devbox/internal/shellgen"
25+
"go.jetpack.io/devbox/internal/telemetry"
2526
"golang.org/x/exp/slices"
2627

2728
"go.jetpack.io/devbox/internal/boxcli/usererr"
@@ -39,7 +40,6 @@ import (
3940
"go.jetpack.io/devbox/internal/redact"
4041
"go.jetpack.io/devbox/internal/searcher"
4142
"go.jetpack.io/devbox/internal/services"
42-
"go.jetpack.io/devbox/internal/telemetry"
4343
"go.jetpack.io/devbox/internal/ux"
4444
"go.jetpack.io/devbox/internal/wrapnix"
4545
)
@@ -185,18 +185,13 @@ func (d *Devbox) Shell(ctx context.Context) error {
185185
return err
186186
}
187187

188-
shellStartTime := envir.GetValueOrDefault(
189-
envir.DevboxShellStartTime,
190-
telemetry.UnixTimestampFromTime(telemetry.CommandStartTime()),
191-
)
192-
193188
opts := []ShellOption{
194189
WithHooksFilePath(shellgen.ScriptPath(d.ProjectDir(), shellgen.HooksFilename)),
195190
WithProfile(profileDir),
196191
WithHistoryFile(filepath.Join(d.projectDir, shellHistoryFile)),
197192
WithProjectDir(d.projectDir),
198193
WithEnvVariables(envs),
199-
WithShellStartTime(shellStartTime),
194+
WithShellStartTime(telemetry.ShellStart()),
200195
}
201196

202197
shell, err := NewDevboxShell(d, opts...)
@@ -985,7 +980,6 @@ func (d *Devbox) checkOldEnvrc() error {
985980
"Run `devbox generate direnv --force` to update it.\n"+
986981
"Or silence this warning by setting DEVBOX_NO_ENVRC_UPDATE=1 env variable.\n",
987982
)
988-
989983
}
990984
}
991985
return nil
@@ -1058,7 +1052,6 @@ func (d *Devbox) setCommonHelperEnvVars(env map[string]string) {
10581052
// buildInputs
10591053
func (d *Devbox) NixBins(ctx context.Context) ([]string, error) {
10601054
env, err := d.nixEnv(ctx)
1061-
10621055
if err != nil {
10631056
return nil, err
10641057
}
@@ -1132,7 +1125,6 @@ func (d *Devbox) parseEnvAndExcludeSpecialCases(currentEnv []string) (map[string
11321125

11331126
// ExportifySystemPathWithoutWrappers is a small utility to filter WrapperBin paths from PATH
11341127
func ExportifySystemPathWithoutWrappers() string {
1135-
11361128
path := []string{}
11371129
for _, p := range strings.Split(os.Getenv("PATH"), string(filepath.ListSeparator)) {
11381130
// Intentionally do not include projectDir with plugin.WrapperBinPath so that

internal/impl/shell.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ import (
1313
"path/filepath"
1414
"strings"
1515
"text/template"
16+
"time"
1617

1718
"github.com/alessio/shellescape"
1819
"github.com/pkg/errors"
1920
"go.jetpack.io/devbox/internal/boxcli/featureflag"
2021
"go.jetpack.io/devbox/internal/shenv"
22+
"go.jetpack.io/devbox/internal/telemetry"
2123

2224
"go.jetpack.io/devbox/internal/debug"
2325
"go.jetpack.io/devbox/internal/envir"
@@ -65,7 +67,7 @@ type DevboxShell struct {
6567
historyFile string
6668

6769
// shellStartTime is the unix timestamp for when the command was invoked
68-
shellStartTime string
70+
shellStartTime time.Time
6971
}
7072

7173
type ShellOption func(*DevboxShell)
@@ -197,9 +199,9 @@ func WithProjectDir(projectDir string) ShellOption {
197199
}
198200
}
199201

200-
func WithShellStartTime(time string) ShellOption {
202+
func WithShellStartTime(t time.Time) ShellOption {
201203
return func(s *DevboxShell) {
202-
s.shellStartTime = time
204+
s.shellStartTime = t
203205
}
204206
}
205207

@@ -331,7 +333,7 @@ func (s *DevboxShell) writeDevboxShellrc() (path string, err error) {
331333
OriginalInitPath: s.userShellrcPath,
332334
HooksFilePath: s.hooksFilePath,
333335
ShellName: string(s.name),
334-
ShellStartTime: s.shellStartTime,
336+
ShellStartTime: telemetry.FormatShellStart(s.shellStartTime),
335337
HistoryFile: strings.TrimSpace(s.historyFile),
336338
ExportEnv: exportify(s.env),
337339
PromptHookEnabled: featureflag.PromptHook.Enabled(),

0 commit comments

Comments
 (0)