Skip to content

Commit 2ddf185

Browse files
authored
telemetry: report errors in separate process (#859)
1 parent ca43a9b commit 2ddf185

File tree

4 files changed

+102
-17
lines changed

4 files changed

+102
-17
lines changed

internal/boxcli/midcobra/telemetry.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ func (m *telemetryMiddleware) postRun(cmd *cobra.Command, args []string, runErr
7676
meta.InCloud = envir.IsDevboxCloud()
7777
telemetry.Error(runErr, meta)
7878

79+
if !telemetry.Enabled() {
80+
return
81+
}
7982
evt := m.newEventIfValid(cmd, args, runErr)
8083
if evt == nil {
8184
return

internal/boxcli/root.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"go.jetpack.io/devbox/internal/boxcli/midcobra"
1414
"go.jetpack.io/devbox/internal/cloud/openssh/sshshim"
1515
"go.jetpack.io/devbox/internal/debug"
16+
"go.jetpack.io/devbox/internal/telemetry"
1617
"go.jetpack.io/devbox/internal/vercheck"
1718
)
1819

@@ -83,6 +84,10 @@ func Main() {
8384
code := sshshim.Execute(os.Args)
8485
os.Exit(code)
8586
}
87+
if len(os.Args) > 1 && os.Args[1] == "bug" {
88+
telemetry.ReportErrors()
89+
return
90+
}
8691
code := Execute(context.Background(), os.Args[1:])
8792
os.Exit(code)
8893
}

internal/telemetry/sentry.go

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ package telemetry
33
import (
44
"crypto/rand"
55
"encoding/hex"
6+
"encoding/json"
67
"errors"
8+
"os"
9+
"os/exec"
710
"path"
811
"path/filepath"
912
"reflect"
1013
"runtime"
1114
"runtime/debug"
1215
"strings"
16+
"sync/atomic"
1317
"time"
1418
"unicode"
1519
"unicode/utf8"
@@ -18,6 +22,7 @@ import (
1822
pkgerrors "github.com/pkg/errors"
1923
"go.jetpack.io/devbox/internal/build"
2024
"go.jetpack.io/devbox/internal/redact"
25+
"go.jetpack.io/devbox/internal/xdg"
2126
)
2227

2328
var ExecutionID string
@@ -34,15 +39,71 @@ func init() {
3439
ExecutionID = hex.EncodeToString(id)
3540
}
3641

42+
var needsFlush atomic.Bool
3743
var started bool
3844

3945
// Start enables telemetry for the current program.
4046
func Start(appName string) {
47+
if started || DoNotTrack() {
48+
return
49+
}
50+
started = initSentry(appName)
51+
}
52+
53+
// Stop stops gathering telemetry and flushes buffered events to disk.
54+
func Stop() {
55+
if !started || !needsFlush.Load() {
56+
return
57+
}
58+
59+
// Report errors in a separate process so we don't block exiting.
60+
exe, err := os.Executable()
61+
if err == nil {
62+
_ = exec.Command(exe, "bug").Start()
63+
}
64+
started = false
65+
}
66+
67+
var errorBufferDir = xdg.StateSubpath(filepath.FromSlash("devbox/sentry"))
68+
69+
func ReportErrors() {
70+
if !initSentry(AppDevbox) {
71+
return
72+
}
73+
74+
dirEntries, err := os.ReadDir(errorBufferDir)
75+
if err != nil {
76+
return
77+
}
78+
for _, entry := range dirEntries {
79+
if !entry.Type().IsRegular() || filepath.Ext(entry.Name()) != ".json" {
80+
continue
81+
}
82+
83+
path := filepath.Join(errorBufferDir, entry.Name())
84+
data, err := os.ReadFile(path)
85+
// Always delete the file so we don't end up with an infinitely growing
86+
// backlog of errors.
87+
_ = os.Remove(path)
88+
if err != nil {
89+
continue
90+
}
91+
92+
event := &sentry.Event{}
93+
if err := json.Unmarshal(data, event); err != nil {
94+
continue
95+
}
96+
sentry.CaptureEvent(event)
97+
}
98+
sentry.Flush(3 * time.Second)
99+
}
100+
101+
func initSentry(appName string) bool {
41102
if appName == "" {
42103
panic("telemetry.Start: app name is empty")
43104
}
44-
if started || DoNotTrack() {
45-
return
105+
if build.SentryDSN == "" {
106+
return false
46107
}
47108

48109
transport := sentry.NewHTTPTransport()
@@ -51,27 +112,19 @@ func Start(appName string) {
51112
if build.IsDev {
52113
environment = "development"
53114
}
54-
_ = sentry.Init(sentry.ClientOptions{
115+
err := sentry.Init(sentry.ClientOptions{
55116
Dsn: build.SentryDSN,
56117
Environment: environment,
57118
Release: appName + "@" + build.Version,
58119
Transport: transport,
59120
TracesSampleRate: 1,
60121
BeforeSend: func(event *sentry.Event, _ *sentry.EventHint) *sentry.Event {
61-
event.ServerName = "" // redact the hostname, which the SDK automatically adds
122+
// redact the hostname, which the SDK automatically adds
123+
event.ServerName = ""
62124
return event
63125
},
64126
})
65-
started = true
66-
}
67-
68-
// Stop stops gathering telemetry and flushes buffered events to the server.
69-
func Stop() {
70-
if !started {
71-
return
72-
}
73-
sentry.Flush(2 * time.Second)
74-
started = false
127+
return err == nil
75128
}
76129

77130
type Metadata struct {
@@ -186,7 +239,29 @@ func Error(err error, meta Metadata) {
186239
if sentryCtx := meta.pkgContext(); len(sentryCtx) > 0 {
187240
event.Contexts["Devbox Packages"] = sentryCtx
188241
}
189-
sentry.CaptureEvent(event)
242+
bufferEvent(event)
243+
}
244+
245+
// bufferEvent buffers a Sentry event to disk so that ReportErrors can upload
246+
// it later.
247+
func bufferEvent(event *sentry.Event) {
248+
data, err := json.Marshal(event)
249+
if err != nil {
250+
return
251+
}
252+
253+
file := filepath.Join(errorBufferDir, string(event.EventID)+".json")
254+
err = os.WriteFile(file, data, 0600)
255+
if errors.Is(err, os.ErrNotExist) {
256+
// XDG specifies perms 0700.
257+
if err := os.MkdirAll(errorBufferDir, 0700); err != nil {
258+
return
259+
}
260+
err = os.WriteFile(file, data, 0600)
261+
}
262+
if err == nil {
263+
needsFlush.Store(true)
264+
}
190265
}
191266

192267
func newSentryException(err error) []sentry.Exception {

internal/telemetry/telemetry.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strconv"
66

77
"github.com/denisbrodbeck/machineid"
8+
"go.jetpack.io/devbox/internal/build"
89
)
910

1011
var DeviceID string
@@ -15,8 +16,9 @@ const (
1516
)
1617

1718
func init() {
18-
19-
if DoNotTrack() {
19+
// TODO(gcurtis): clean this up so that Sentry and Segment use the same
20+
// start/stop functions.
21+
if DoNotTrack() || build.TelemetryKey == "" {
2022
return
2123
}
2224
enabled = true

0 commit comments

Comments
 (0)