@@ -3,13 +3,17 @@ package telemetry
3
3
import (
4
4
"crypto/rand"
5
5
"encoding/hex"
6
+ "encoding/json"
6
7
"errors"
8
+ "os"
9
+ "os/exec"
7
10
"path"
8
11
"path/filepath"
9
12
"reflect"
10
13
"runtime"
11
14
"runtime/debug"
12
15
"strings"
16
+ "sync/atomic"
13
17
"time"
14
18
"unicode"
15
19
"unicode/utf8"
@@ -18,6 +22,7 @@ import (
18
22
pkgerrors "github.com/pkg/errors"
19
23
"go.jetpack.io/devbox/internal/build"
20
24
"go.jetpack.io/devbox/internal/redact"
25
+ "go.jetpack.io/devbox/internal/xdg"
21
26
)
22
27
23
28
var ExecutionID string
@@ -34,15 +39,71 @@ func init() {
34
39
ExecutionID = hex .EncodeToString (id )
35
40
}
36
41
42
+ var needsFlush atomic.Bool
37
43
var started bool
38
44
39
45
// Start enables telemetry for the current program.
40
46
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 {
41
102
if appName == "" {
42
103
panic ("telemetry.Start: app name is empty" )
43
104
}
44
- if started || DoNotTrack () {
45
- return
105
+ if build . SentryDSN == "" {
106
+ return false
46
107
}
47
108
48
109
transport := sentry .NewHTTPTransport ()
@@ -51,27 +112,19 @@ func Start(appName string) {
51
112
if build .IsDev {
52
113
environment = "development"
53
114
}
54
- _ = sentry .Init (sentry.ClientOptions {
115
+ err : = sentry .Init (sentry.ClientOptions {
55
116
Dsn : build .SentryDSN ,
56
117
Environment : environment ,
57
118
Release : appName + "@" + build .Version ,
58
119
Transport : transport ,
59
120
TracesSampleRate : 1 ,
60
121
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 = ""
62
124
return event
63
125
},
64
126
})
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
75
128
}
76
129
77
130
type Metadata struct {
@@ -186,7 +239,29 @@ func Error(err error, meta Metadata) {
186
239
if sentryCtx := meta .pkgContext (); len (sentryCtx ) > 0 {
187
240
event .Contexts ["Devbox Packages" ] = sentryCtx
188
241
}
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
+ }
190
265
}
191
266
192
267
func newSentryException (err error ) []sentry.Exception {
0 commit comments