@@ -3,13 +3,17 @@ package telemetry
33import (
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
2328var ExecutionID string
@@ -34,15 +39,71 @@ func init() {
3439 ExecutionID = hex .EncodeToString (id )
3540}
3641
42+ var needsFlush atomic.Bool
3743var started bool
3844
3945// Start enables telemetry for the current program.
4046func 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
77130type 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
192267func newSentryException (err error ) []sentry.Exception {
0 commit comments