Skip to content

Commit d329f13

Browse files
committed
telemetry: report segment events asynchronously
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. 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 8d000ba commit d329f13

File tree

12 files changed

+411
-515
lines changed

12 files changed

+411
-515
lines changed

internal/boxcli/log.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/spf13/cobra"
88

99
"go.jetpack.io/devbox/internal/boxcli/usererr"
10+
"go.jetpack.io/devbox/internal/envir"
1011
"go.jetpack.io/devbox/internal/telemetry"
1112
)
1213

@@ -27,11 +28,21 @@ func doLogCommand(cmd *cobra.Command, args []string) error {
2728
return usererr.New("expect an <event-name> arg for command: %s", cmd.CommandPath())
2829
}
2930

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

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func Main() {
111111
}
112112

113113
if len(os.Args) > 1 && os.Args[1] == "bug" {
114-
telemetry.ReportErrors()
114+
telemetry.Upload()
115115
return
116116
}
117117

internal/cloud/cloud.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828
"go.jetpack.io/devbox/internal/debug"
2929
"go.jetpack.io/devbox/internal/envir"
3030
"go.jetpack.io/devbox/internal/services"
31-
"go.jetpack.io/devbox/internal/telemetry"
3231
"go.jetpack.io/devbox/internal/ux/stepper"
3332
)
3433

@@ -445,7 +444,7 @@ func shell(username, hostname, projectDir string, shellStartTime time.Time) erro
445444
cmd := &openssh.Cmd{
446445
DestinationAddr: hostname,
447446
PathInVM: absoluteProjectPathInVM(username, projectPath),
448-
ShellStartTime: telemetry.UnixTimestampFromTime(shellStartTime),
447+
ShellStartTime: envir.FormatShellStart(shellStartTime),
449448
Username: username,
450449
}
451450
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: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,9 @@ package envir
66
import (
77
"os"
88
"strconv"
9+
"time"
910
)
1011

11-
func IsCLICloudShell() bool { // TODO: not used any more
12-
cliCloudShell, _ := strconv.ParseBool(os.Getenv(devboxCLICloudShell))
13-
return cliCloudShell
14-
}
15-
1612
func IsDevboxCloud() bool {
1713
return os.Getenv(DevboxRegion) != ""
1814
}
@@ -43,6 +39,28 @@ func IsCI() bool {
4339
return ci && err == nil
4440
}
4541

42+
func ShellStart() time.Time {
43+
return ParseShellStart(os.Getenv(DevboxShellStartTime))
44+
}
45+
46+
func FormatShellStart(t time.Time) string {
47+
if t.IsZero() {
48+
return ""
49+
}
50+
return strconv.FormatInt(t.Unix(), 10)
51+
}
52+
53+
func ParseShellStart(s string) time.Time {
54+
if s == "" {
55+
return time.Time{}
56+
}
57+
unix, err := strconv.ParseInt(s, 10, 64)
58+
if err != nil {
59+
return time.Time{}
60+
}
61+
return time.Unix(unix, 0)
62+
}
63+
4664
// GetValueOrDefault gets the value of an environment variable.
4765
// If it's empty, it will return the given default value instead.
4866
func GetValueOrDefault(key, def string) string {

internal/impl/devbox.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ import (
3939
"go.jetpack.io/devbox/internal/redact"
4040
"go.jetpack.io/devbox/internal/searcher"
4141
"go.jetpack.io/devbox/internal/services"
42-
"go.jetpack.io/devbox/internal/telemetry"
4342
"go.jetpack.io/devbox/internal/ux"
4443
"go.jetpack.io/devbox/internal/wrapnix"
4544
)
@@ -185,18 +184,13 @@ func (d *Devbox) Shell(ctx context.Context) error {
185184
return err
186185
}
187186

188-
shellStartTime := envir.GetValueOrDefault(
189-
envir.DevboxShellStartTime,
190-
telemetry.UnixTimestampFromTime(telemetry.CommandStartTime()),
191-
)
192-
193187
opts := []ShellOption{
194188
WithHooksFilePath(shellgen.ScriptPath(d.ProjectDir(), shellgen.HooksFilename)),
195189
WithProfile(profileDir),
196190
WithHistoryFile(filepath.Join(d.projectDir, shellHistoryFile)),
197191
WithProjectDir(d.projectDir),
198192
WithEnvVariables(envs),
199-
WithShellStartTime(shellStartTime),
193+
WithShellStartTime(envir.ShellStart()),
200194
}
201195

202196
shell, err := NewDevboxShell(d, opts...)
@@ -985,7 +979,6 @@ func (d *Devbox) checkOldEnvrc() error {
985979
"Run `devbox generate direnv --force` to update it.\n"+
986980
"Or silence this warning by setting DEVBOX_NO_ENVRC_UPDATE=1 env variable.\n",
987981
)
988-
989982
}
990983
}
991984
return nil
@@ -1058,7 +1051,6 @@ func (d *Devbox) setCommonHelperEnvVars(env map[string]string) {
10581051
// buildInputs
10591052
func (d *Devbox) NixBins(ctx context.Context) ([]string, error) {
10601053
env, err := d.nixEnv(ctx)
1061-
10621054
if err != nil {
10631055
return nil, err
10641056
}
@@ -1132,7 +1124,6 @@ func (d *Devbox) parseEnvAndExcludeSpecialCases(currentEnv []string) (map[string
11321124

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

internal/impl/shell.go

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

1718
"github.com/alessio/shellescape"
1819
"github.com/pkg/errors"
@@ -65,7 +66,7 @@ type DevboxShell struct {
6566
historyFile string
6667

6768
// shellStartTime is the unix timestamp for when the command was invoked
68-
shellStartTime string
69+
shellStartTime time.Time
6970
}
7071

7172
type ShellOption func(*DevboxShell)
@@ -197,9 +198,9 @@ func WithProjectDir(projectDir string) ShellOption {
197198
}
198199
}
199200

200-
func WithShellStartTime(time string) ShellOption {
201+
func WithShellStartTime(t time.Time) ShellOption {
201202
return func(s *DevboxShell) {
202-
s.shellStartTime = time
203+
s.shellStartTime = t
203204
}
204205
}
205206

@@ -331,7 +332,7 @@ func (s *DevboxShell) writeDevboxShellrc() (path string, err error) {
331332
OriginalInitPath: s.userShellrcPath,
332333
HooksFilePath: s.hooksFilePath,
333334
ShellName: string(s.name),
334-
ShellStartTime: s.shellStartTime,
335+
ShellStartTime: envir.FormatShellStart(s.shellStartTime),
335336
HistoryFile: strings.TrimSpace(s.historyFile),
336337
ExportEnv: exportify(s.env),
337338
PromptHookEnabled: featureflag.PromptHook.Enabled(),

0 commit comments

Comments
 (0)