Skip to content

Commit e7b03a0

Browse files
julianknutsenclaude
andcommitted
feat: integrate Sentry for error tracking and performance monitoring
Go backend: sentry-go SDK with HTTP middleware for panic recovery and request tracing. Explicit captures on upstream outages (browse/detail 503s) and Nango infrastructure failures. React frontend: @sentry/react with ErrorBoundary, browser tracing, and session replay on errors. API client captures 5xx responses. Vite plugin uploads source maps when SENTRY_AUTH_TOKEN is set. All SDKs are no-ops when SENTRY_DSN is unset, so self-sovereign deployments are unaffected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1eb9d41 commit e7b03a0

11 files changed

Lines changed: 704 additions & 27 deletions

File tree

cmd/wl/cmd_serve.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import (
2525
"github.com/gastownhall/wasteland/internal/sdk"
2626
"github.com/gastownhall/wasteland/internal/style"
2727
"github.com/gastownhall/wasteland/web"
28+
"github.com/getsentry/sentry-go"
29+
sentryhttp "github.com/getsentry/sentry-go/http"
2830
"github.com/spf13/cobra"
2931
)
3032

@@ -83,10 +85,28 @@ func listenAndServeGraceful(srv *http.Server) error {
8385
}
8486
}
8587

88+
func initSentry(environment string) {
89+
dsn := os.Getenv("SENTRY_DSN")
90+
if dsn == "" {
91+
return
92+
}
93+
if err := sentry.Init(sentry.ClientOptions{
94+
Dsn: dsn,
95+
Environment: environment,
96+
Release: version,
97+
TracesSampleRate: 0.2,
98+
}); err != nil {
99+
slog.Error("sentry init failed", "error", err)
100+
}
101+
}
102+
86103
func runServe(cmd *cobra.Command, stdout, stderr io.Writer) error {
87104
logger := slog.New(slog.NewJSONHandler(stdout, nil))
88105
slog.SetDefault(logger)
89106

107+
initSentry("self-sovereign")
108+
defer sentry.Flush(2 * time.Second)
109+
90110
port := resolvePort(cmd)
91111
devMode, _ := cmd.Flags().GetBool("dev")
92112

@@ -209,7 +229,8 @@ func runServe(cmd *cobra.Command, stdout, stderr io.Writer) error {
209229
defer rateLimiter.Stop()
210230
generalRL := api.RateLimit(rateLimiter)
211231
bodyLimit := api.MaxBytesBody(64 << 10) // 64 KB
212-
handler := api.RequestLog(logger)(api.SecurityHeaders(generalRL(bodyLimit(api.SPAHandler(server, web.Assets)))))
232+
sentryMiddleware := sentryhttp.New(sentryhttp.Options{Repanic: true})
233+
handler := sentryMiddleware.Handle(api.RequestLog(logger)(api.SecurityHeaders(generalRL(bodyLimit(api.SPAHandler(server, web.Assets))))))
213234
if devMode {
214235
handler = api.CORSMiddleware(handler)
215236
}
@@ -227,6 +248,13 @@ func runServeHosted(cmd *cobra.Command, stdout, _ io.Writer) error {
227248
port := resolvePort(cmd)
228249
devMode, _ := cmd.Flags().GetBool("dev")
229250

251+
environment := os.Getenv("WL_ENVIRONMENT")
252+
if environment == "" {
253+
environment = "production"
254+
}
255+
initSentry(environment)
256+
defer sentry.Flush(2 * time.Second)
257+
230258
// Read required env vars.
231259
nangoSecretKey := os.Getenv("NANGO_SECRET_KEY")
232260
if nangoSecretKey == "" {
@@ -236,7 +264,6 @@ func runServeHosted(cmd *cobra.Command, stdout, _ io.Writer) error {
236264
if sessionSecret == "" {
237265
return fmt.Errorf("WL_SESSION_SECRET environment variable is required for hosted mode")
238266
}
239-
environment := os.Getenv("WL_ENVIRONMENT")
240267

241268
// Optional env vars with defaults.
242269
nangoBaseURL := os.Getenv("NANGO_BASE_URL")
@@ -293,7 +320,8 @@ func runServeHosted(cmd *cobra.Command, stdout, _ io.Writer) error {
293320
defer hostedRateLimiter.Stop()
294321
generalRL := api.RateLimit(hostedRateLimiter)
295322
bodyLimit := api.MaxBytesBody(64 << 10) // 64 KB
296-
handler := api.RequestLog(logger)(api.SecurityHeaders(generalRL(bodyLimit(hostedServer.Handler(apiServer, web.Assets)))))
323+
sentryMiddleware := sentryhttp.New(sentryhttp.Options{Repanic: true})
324+
handler := sentryMiddleware.Handle(api.RequestLog(logger)(api.SecurityHeaders(generalRL(bodyLimit(hostedServer.Handler(apiServer, web.Assets))))))
297325
if devMode {
298326
handler = api.CORSMiddleware(handler)
299327
}

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/charmbracelet/bubbles v1.0.0
77
github.com/charmbracelet/bubbletea v1.3.10
88
github.com/charmbracelet/lipgloss v1.1.0
9+
github.com/getsentry/sentry-go v0.43.0
910
github.com/mattn/go-isatty v0.0.20
1011
github.com/rogpeppe/go-internal v1.14.1
1112
github.com/spf13/cobra v1.10.2
@@ -33,6 +34,6 @@ require (
3334
github.com/spf13/pflag v1.0.9 // indirect
3435
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
3536
golang.org/x/sys v0.38.0 // indirect
36-
golang.org/x/text v0.3.8 // indirect
37+
golang.org/x/text v0.14.0 // indirect
3738
golang.org/x/tools v0.26.0 // indirect
3839
)

go.sum

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,16 @@ github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEX
2323
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
2424
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
2525
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
26+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
27+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2628
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
2729
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
30+
github.com/getsentry/sentry-go v0.43.0 h1:XbXLpFicpo8HmBDaInk7dum18G9KSLcjZiyUKS+hLW4=
31+
github.com/getsentry/sentry-go v0.43.0/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0=
32+
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
33+
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
34+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
35+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
2836
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
2937
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
3038
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
@@ -41,6 +49,12 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
4149
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
4250
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
4351
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
52+
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
53+
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
54+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
55+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
56+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
57+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
4458
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
4559
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
4660
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -50,17 +64,23 @@ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
5064
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
5165
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
5266
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
67+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
68+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
5369
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
5470
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
71+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
72+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
5573
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
5674
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
5775
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
5876
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
5977
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
6078
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
6179
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
62-
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
63-
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
80+
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
81+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
6482
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
6583
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
6684
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
85+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
86+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/api/handlers.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/gastownhall/wasteland/internal/commons"
1212
"github.com/gastownhall/wasteland/internal/sdk"
13+
"github.com/getsentry/sentry-go"
1314
)
1415

1516
// resolveClient extracts the sdk.Client from the request. Returns false if
@@ -65,6 +66,7 @@ func (s *Server) handleBrowse(w http.ResponseWriter, r *http.Request) {
6566
// No stale data — return a 503 with a clear outage message.
6667
msg := "Upstream database is temporarily unavailable — please try again in a moment."
6768
slog.Error("browse failed with no stale data", "error", err)
69+
sentry.CaptureException(err)
6870
writeJSON(w, http.StatusServiceUnavailable, ErrorResponse{Error: msg})
6971
return
7072
}
@@ -106,6 +108,7 @@ func (s *Server) handleDetail(w http.ResponseWriter, r *http.Request) {
106108
}
107109
msg := "Upstream database is temporarily unavailable — please try again in a moment."
108110
slog.Error("detail failed with no stale data", "error", err)
111+
sentry.CaptureException(err)
109112
writeJSON(w, http.StatusServiceUnavailable, ErrorResponse{Error: msg})
110113
return
111114
}

internal/hosted/server.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/gastownhall/wasteland/internal/api"
1111
"github.com/gastownhall/wasteland/internal/sdk"
12+
"github.com/getsentry/sentry-go"
1213
)
1314

1415
// Server provides hosted-mode handler composition.
@@ -114,6 +115,7 @@ func (s *Server) handleConnect(w http.ResponseWriter, r *http.Request) {
114115
})
115116
if err := s.nango.SetMetadata(req.ConnectionID, meta); err != nil {
116117
slog.Error("nango: failed to save metadata", "error", err, "connection_id", req.ConnectionID)
118+
sentry.CaptureException(err)
117119
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save config: " + err.Error()})
118120
return
119121
}
@@ -133,6 +135,7 @@ func (s *Server) handleConnect(w http.ResponseWriter, r *http.Request) {
133135
sessionID, err := s.sessions.Create(req.ConnectionID)
134136
if err != nil {
135137
slog.Error("failed to create session", "error", err, "connection_id", req.ConnectionID)
138+
sentry.CaptureException(err)
136139
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to create session: " + err.Error()})
137140
return
138141
}
@@ -231,6 +234,7 @@ func (s *Server) handleConnectSession(w http.ResponseWriter, r *http.Request) {
231234
token, err := s.nango.CreateConnectSession(req.EndUserID)
232235
if err != nil {
233236
slog.Error("nango: failed to create connect session", "error", err, "end_user_id", req.EndUserID)
237+
sentry.CaptureException(err)
234238
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to create session: " + err.Error()})
235239
return
236240
}
@@ -296,6 +300,7 @@ func (s *Server) handleJoin(w http.ResponseWriter, r *http.Request) {
296300
apiKey, meta, err := s.nango.GetConnection(session.ConnectionID)
297301
if err != nil {
298302
slog.Error("nango: failed to read metadata", "error", err, "connection_id", session.ConnectionID)
303+
sentry.CaptureException(err)
299304
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to read metadata: " + err.Error()})
300305
return
301306
}
@@ -313,6 +318,7 @@ func (s *Server) handleJoin(w http.ResponseWriter, r *http.Request) {
313318

314319
if err := s.nango.SetMetadata(session.ConnectionID, meta); err != nil {
315320
slog.Error("nango: failed to save metadata", "error", err, "connection_id", session.ConnectionID)
321+
sentry.CaptureException(err)
316322
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save metadata: " + err.Error()})
317323
return
318324
}
@@ -367,6 +373,7 @@ func (s *Server) handleLeaveWasteland(w http.ResponseWriter, r *http.Request) {
367373
_, meta, err := s.nango.GetConnection(session.ConnectionID)
368374
if err != nil {
369375
slog.Error("nango: failed to read metadata", "error", err, "connection_id", session.ConnectionID)
376+
sentry.CaptureException(err)
370377
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to read metadata: " + err.Error()})
371378
return
372379
}
@@ -387,6 +394,7 @@ func (s *Server) handleLeaveWasteland(w http.ResponseWriter, r *http.Request) {
387394

388395
if err := s.nango.SetMetadata(session.ConnectionID, meta); err != nil {
389396
slog.Error("nango: failed to save metadata", "error", err, "connection_id", session.ConnectionID)
397+
sentry.CaptureException(err)
390398
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save metadata: " + err.Error()})
391399
return
392400
}

0 commit comments

Comments
 (0)