From b04ed68832704c4162239f2a199ab1e513b0be89 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 20 Sep 2023 02:39:14 +0200 Subject: [PATCH] feat(backend): add sentry integration --- backend/main.go | 9 +++++++ backend/routes/v1/auth/discord.go | 14 +++++----- backend/routes/v1/auth/fedi_mastodon.go | 12 ++++----- backend/routes/v1/auth/fedi_misskey.go | 10 +++---- backend/routes/v1/auth/google.go | 12 ++++----- backend/routes/v1/auth/tumblr.go | 12 ++++----- backend/routes/v1/member/create_member.go | 12 ++++----- backend/routes/v1/member/delete_member.go | 2 +- backend/routes/v1/member/get_member.go | 14 +++++----- backend/routes/v1/member/get_members.go | 5 ++-- backend/routes/v1/member/patch_member.go | 20 +++++++------- backend/routes/v1/user/export.go | 3 ++- backend/routes/v1/user/get_user.go | 17 ++++++------ backend/routes/v1/user/patch_user.go | 22 +++++++-------- backend/routes/v2/user/get_settings.go | 3 ++- backend/server/errors.go | 33 ++++++++++++++++++----- docs/api/errors.md | 13 ++++----- go.mod | 1 + go.sum | 6 ++++- 19 files changed, 130 insertions(+), 90 deletions(-) diff --git a/backend/main.go b/backend/main.go index 008e80f..0cb68a8 100644 --- a/backend/main.go +++ b/backend/main.go @@ -11,6 +11,7 @@ import ( "codeberg.org/pronounscc/pronouns.cc/backend/server" "github.com/davidbyttow/govips/v2/vips" + "github.com/getsentry/sentry-go" "github.com/go-chi/render" _ "github.com/joho/godotenv/autoload" "github.com/urfave/cli/v2" @@ -23,6 +24,14 @@ var Command = &cli.Command{ } func run(c *cli.Context) error { + // initialize sentry + if dsn := os.Getenv("SENTRY_DSN"); dsn != "" { + sentry.Init(sentry.ClientOptions{ + Dsn: dsn, + Release: server.Tag, + }) + } + // set vips log level to WARN, else it will spam logs on info level vips.LoggingSettings(nil, vips.LogLevelWarning) diff --git a/backend/routes/v1/auth/discord.go b/backend/routes/v1/auth/discord.go index f22518c..2084b52 100644 --- a/backend/routes/v1/auth/discord.go +++ b/backend/routes/v1/auth/discord.go @@ -61,7 +61,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error { // if the state can't be validated, return if valid, err := s.validateCSRFState(ctx, decoded.State); !valid { if err != nil { - return err + return errors.Wrap(err, "validating state") } return server.APIError{Code: server.ErrInvalidState} @@ -79,7 +79,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error { dg, _ := discordgo.New(token.Type() + " " + token.AccessToken) du, err := dg.User("@me") if err != nil { - return err + return errors.Wrap(err, "getting discord user") } u, err := s.DB.DiscordUser(ctx, du.ID) @@ -90,7 +90,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error { err = s.saveUndeleteToken(ctx, u.ID, token) if err != nil { log.Errorf("saving undelete token: %v", err) - return err + return errors.Wrap(err, "saving undelete token") } render.JSON(w, r, discordCallbackResponse{ @@ -114,7 +114,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error { tokenID := xid.New() token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true) if err != nil { - return err + return errors.Wrap(err, "creating token") } // save token to database @@ -137,7 +137,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error { return nil } else if err != db.ErrUserNotFound { // internal error - return err + return errors.Wrap(err, "getting user") } // no user found, so save a ticket + save their Discord info in Redis @@ -145,7 +145,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error { err = s.DB.SetJSON(ctx, "discord:"+ticket, du, "EX", "600") if err != nil { log.Errorf("setting Discord user for ticket %q: %v", ticket, err) - return err + return errors.Wrap(err, "caching discord user for ticket") } render.JSON(w, r, discordCallbackResponse{ @@ -278,7 +278,7 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error { valid, taken, err := s.DB.UsernameTaken(ctx, req.Username) if err != nil { - return err + return errors.Wrap(err, "checking if username is taken") } if !valid { return server.APIError{Code: server.ErrInvalidUsername} diff --git a/backend/routes/v1/auth/fedi_mastodon.go b/backend/routes/v1/auth/fedi_mastodon.go index 1cae10b..bb07958 100644 --- a/backend/routes/v1/auth/fedi_mastodon.go +++ b/backend/routes/v1/auth/fedi_mastodon.go @@ -54,7 +54,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error // if the state can't be validated, return if valid, err := s.validateCSRFState(ctx, decoded.State); !valid { if err != nil { - return err + return errors.Wrap(err, "validating state") } return server.APIError{Code: server.ErrInvalidState} @@ -111,7 +111,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error err = s.saveUndeleteToken(ctx, u.ID, token) if err != nil { log.Errorf("saving undelete token: %v", err) - return err + return errors.Wrap(err, "saving undelete token") } render.JSON(w, r, fediCallbackResponse{ @@ -135,7 +135,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error tokenID := xid.New() token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true) if err != nil { - return err + return errors.Wrap(err, "creating token") } // save token to database @@ -158,7 +158,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error return nil } else if err != db.ErrUserNotFound { // internal error - return err + return errors.Wrap(err, "getting user") } // no user found, so save a ticket + save their Mastodon info in Redis @@ -166,7 +166,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error err = s.DB.SetJSON(ctx, "mastodon:"+ticket, mu, "EX", "600") if err != nil { log.Errorf("setting mastoAPI user for ticket %q: %v", ticket, err) - return err + return errors.Wrap(err, "setting user for ticket") } render.JSON(w, r, fediCallbackResponse{ @@ -306,7 +306,7 @@ func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error { valid, taken, err := s.DB.UsernameTaken(ctx, req.Username) if err != nil { - return err + return errors.Wrap(err, "checking if username is taken") } if !valid { return server.APIError{Code: server.ErrInvalidUsername} diff --git a/backend/routes/v1/auth/fedi_misskey.go b/backend/routes/v1/auth/fedi_misskey.go index 864b852..ebf6e22 100644 --- a/backend/routes/v1/auth/fedi_misskey.go +++ b/backend/routes/v1/auth/fedi_misskey.go @@ -90,7 +90,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error { err = s.saveUndeleteToken(ctx, u.ID, token) if err != nil { log.Errorf("saving undelete token: %v", err) - return err + return errors.Wrap(err, "saving undelete token") } render.JSON(w, r, fediCallbackResponse{ @@ -114,7 +114,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error { tokenID := xid.New() token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true) if err != nil { - return err + return errors.Wrap(err, "creating token") } // save token to database @@ -137,7 +137,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error { return nil } else if err != db.ErrUserNotFound { // internal error - return err + return errors.Wrap(err, "getting user") } // no user found, so save a ticket + save their Misskey info in Redis @@ -145,7 +145,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error { err = s.DB.SetJSON(ctx, "misskey:"+ticket, mu.User, "EX", "600") if err != nil { log.Errorf("setting misskey user for ticket %q: %v", ticket, err) - return err + return errors.Wrap(err, "setting user for ticket") } render.JSON(w, r, fediCallbackResponse{ @@ -234,7 +234,7 @@ func (s *Server) misskeySignup(w http.ResponseWriter, r *http.Request) error { valid, taken, err := s.DB.UsernameTaken(ctx, req.Username) if err != nil { - return err + return errors.Wrap(err, "checking if username is taken") } if !valid { return server.APIError{Code: server.ErrInvalidUsername} diff --git a/backend/routes/v1/auth/google.go b/backend/routes/v1/auth/google.go index 182c8a6..026a6d1 100644 --- a/backend/routes/v1/auth/google.go +++ b/backend/routes/v1/auth/google.go @@ -60,7 +60,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error { // if the state can't be validated, return if valid, err := s.validateCSRFState(ctx, decoded.State); !valid { if err != nil { - return err + return errors.Wrap(err, "validating state") } return server.APIError{Code: server.ErrInvalidState} @@ -109,7 +109,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error { err = s.saveUndeleteToken(ctx, u.ID, token) if err != nil { log.Errorf("saving undelete token: %v", err) - return err + return errors.Wrap(err, "saving undelete token") } render.JSON(w, r, googleCallbackResponse{ @@ -133,7 +133,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error { tokenID := xid.New() token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true) if err != nil { - return err + return errors.Wrap(err, "creating token") } // save token to database @@ -156,7 +156,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error { return nil } else if err != db.ErrUserNotFound { // internal error - return err + return errors.Wrap(err, "getting user") } // no user found, so save a ticket + save their Google info in Redis @@ -164,7 +164,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error { err = s.DB.SetJSON(ctx, "google:"+ticket, partialGoogleUser{ID: googleID, Email: googleUsername}, "EX", "600") if err != nil { log.Errorf("setting Google user for ticket %q: %v", ticket, err) - return err + return errors.Wrap(err, "setting user for ticket") } render.JSON(w, r, googleCallbackResponse{ @@ -281,7 +281,7 @@ func (s *Server) googleSignup(w http.ResponseWriter, r *http.Request) error { valid, taken, err := s.DB.UsernameTaken(ctx, req.Username) if err != nil { - return err + return errors.Wrap(err, "checking if username is taken") } if !valid { return server.APIError{Code: server.ErrInvalidUsername} diff --git a/backend/routes/v1/auth/tumblr.go b/backend/routes/v1/auth/tumblr.go index d35781a..52dcdec 100644 --- a/backend/routes/v1/auth/tumblr.go +++ b/backend/routes/v1/auth/tumblr.go @@ -77,7 +77,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error { // if the state can't be validated, return if valid, err := s.validateCSRFState(ctx, decoded.State); !valid { if err != nil { - return err + return errors.Wrap(err, "validating state") } return server.APIError{Code: server.ErrInvalidState} @@ -142,7 +142,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error { err = s.saveUndeleteToken(ctx, u.ID, token) if err != nil { log.Errorf("saving undelete token: %v", err) - return err + return errors.Wrap(err, "saving undelete token") } render.JSON(w, r, tumblrCallbackResponse{ @@ -166,7 +166,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error { tokenID := xid.New() token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true) if err != nil { - return err + return errors.Wrap(err, "creating token") } // save token to database @@ -189,7 +189,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error { return nil } else if err != db.ErrUserNotFound { // internal error - return err + return errors.Wrap(err, "getting user") } // no user found, so save a ticket + save their Tumblr info in Redis @@ -197,7 +197,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error { err = s.DB.SetJSON(ctx, "tumblr:"+ticket, tumblrUserInfo{ID: tumblrID, Name: tumblrName}, "EX", "600") if err != nil { log.Errorf("setting Tumblr user for ticket %q: %v", ticket, err) - return err + return errors.Wrap(err, "setting user for ticket") } render.JSON(w, r, tumblrCallbackResponse{ @@ -314,7 +314,7 @@ func (s *Server) tumblrSignup(w http.ResponseWriter, r *http.Request) error { valid, taken, err := s.DB.UsernameTaken(ctx, req.Username) if err != nil { - return err + return errors.Wrap(err, "checking if username is taken") } if !valid { return server.APIError{Code: server.ErrInvalidUsername} diff --git a/backend/routes/v1/member/create_member.go b/backend/routes/v1/member/create_member.go index e26ab74..c94915a 100644 --- a/backend/routes/v1/member/create_member.go +++ b/backend/routes/v1/member/create_member.go @@ -127,14 +127,14 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error return server.APIError{Code: server.ErrMemberNameInUse} } - return err + return errors.Wrap(err, "creating member") } // set names, pronouns, fields err = s.DB.SetMemberNamesPronouns(ctx, tx, m.ID, db.NotNull(cmr.Names), db.NotNull(cmr.Pronouns)) if err != nil { log.Errorf("setting names and pronouns for member %v: %v", m.ID, err) - return err + return errors.Wrap(err, "setting names/pronouns") } m.Names = cmr.Names m.Pronouns = cmr.Pronouns @@ -142,7 +142,7 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error err = s.DB.SetMemberFields(ctx, tx, m.ID, cmr.Fields) if err != nil { log.Errorf("setting fields for member %v: %v", m.ID, err) - return err + return errors.Wrap(err, "setting fields") } if cmr.Avatar != "" { @@ -161,13 +161,13 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error } log.Errorf("converting member avatar: %v", err) - return err + return errors.Wrap(err, "converting avatar") } hash, err := s.DB.WriteMemberAvatar(ctx, m.ID, webp, jpg) if err != nil { log.Errorf("uploading member avatar: %v", err) - return err + return errors.Wrap(err, "uploading avatar") } err = tx.QueryRow(ctx, "UPDATE members SET avatar = $1 WHERE id = $2", hash, m.ID).Scan(&m.Avatar) @@ -180,7 +180,7 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error err = s.DB.UpdateActiveTime(ctx, tx, claims.UserID) if err != nil { log.Errorf("updating last active time for user %v: %v", claims.UserID, err) - return err + return errors.Wrap(err, "updating last active time") } err = tx.Commit(ctx) diff --git a/backend/routes/v1/member/delete_member.go b/backend/routes/v1/member/delete_member.go index 4c2abde..5c8e68a 100644 --- a/backend/routes/v1/member/delete_member.go +++ b/backend/routes/v1/member/delete_member.go @@ -66,7 +66,7 @@ func (s *Server) deleteMember(w http.ResponseWriter, r *http.Request) (err error err = s.DB.UpdateActiveTime(ctx, s.DB, claims.UserID) if err != nil { log.Errorf("updating last active time for user %v: %v", claims.UserID, err) - return err + return errors.Wrap(err, "updating last active time") } render.NoContent(w, r) diff --git a/backend/routes/v1/member/get_member.go b/backend/routes/v1/member/get_member.go index 2a7d5e6..ffea5d5 100644 --- a/backend/routes/v1/member/get_member.go +++ b/backend/routes/v1/member/get_member.go @@ -105,7 +105,7 @@ func (s *Server) getMember(w http.ResponseWriter, r *http.Request) (err error) { u, err := s.DB.User(ctx, m.UserID) if err != nil { - return err + return errors.Wrap(err, "getting user") } if u.DeletedAt != nil { @@ -119,12 +119,12 @@ func (s *Server) getMember(w http.ResponseWriter, r *http.Request) (err error) { fields, err := s.DB.MemberFields(ctx, m.ID) if err != nil { - return err + return errors.Wrap(err, "getting member fields") } flags, err := s.DB.MemberFlags(ctx, m.ID) if err != nil { - return err + return errors.Wrap(err, "getting member flags") } render.JSON(w, r, dbMemberToMember(u, m, fields, flags, isOwnMember)) @@ -159,12 +159,12 @@ func (s *Server) getUserMember(w http.ResponseWriter, r *http.Request) error { fields, err := s.DB.MemberFields(ctx, m.ID) if err != nil { - return err + return errors.Wrap(err, "getting member fields") } flags, err := s.DB.MemberFlags(ctx, m.ID) if err != nil { - return err + return errors.Wrap(err, "getting member flags") } render.JSON(w, r, dbMemberToMember(u, m, fields, flags, isOwnMember)) @@ -189,12 +189,12 @@ func (s *Server) getMeMember(w http.ResponseWriter, r *http.Request) error { fields, err := s.DB.MemberFields(ctx, m.ID) if err != nil { - return err + return errors.Wrap(err, "getting member fields") } flags, err := s.DB.MemberFlags(ctx, m.ID) if err != nil { - return err + return errors.Wrap(err, "getting member flags") } render.JSON(w, r, dbMemberToMember(u, m, fields, flags, true)) diff --git a/backend/routes/v1/member/get_members.go b/backend/routes/v1/member/get_members.go index 6dba566..2442d39 100644 --- a/backend/routes/v1/member/get_members.go +++ b/backend/routes/v1/member/get_members.go @@ -6,6 +6,7 @@ import ( "codeberg.org/pronounscc/pronouns.cc/backend/common" "codeberg.org/pronounscc/pronouns.cc/backend/db" "codeberg.org/pronounscc/pronouns.cc/backend/server" + "emperror.dev/errors" "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/rs/xid" @@ -74,7 +75,7 @@ func (s *Server) getUserMembers(w http.ResponseWriter, r *http.Request) error { ms, err := s.DB.UserMembers(ctx, u.ID, isSelf) if err != nil { - return err + return errors.Wrap(err, "getting members") } render.JSON(w, r, membersToMemberList(ms, isSelf)) @@ -87,7 +88,7 @@ func (s *Server) getMeMembers(w http.ResponseWriter, r *http.Request) error { ms, err := s.DB.UserMembers(ctx, claims.UserID, true) if err != nil { - return err + return errors.Wrap(err, "getting members") } render.JSON(w, r, membersToMemberList(ms, true)) diff --git a/backend/routes/v1/member/patch_member.go b/backend/routes/v1/member/patch_member.go index 8d56a87..f8cee31 100644 --- a/backend/routes/v1/member/patch_member.go +++ b/backend/routes/v1/member/patch_member.go @@ -220,13 +220,13 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error { } log.Errorf("converting member avatar: %v", err) - return err + return errors.Wrap(err, "converting member avatar") } hash, err := s.DB.WriteMemberAvatar(ctx, m.ID, webp, jpg) if err != nil { log.Errorf("uploading member avatar: %v", err) - return err + return errors.Wrap(err, "writing member avatar") } avatarHash = &hash @@ -244,7 +244,7 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error { tx, err := s.DB.Begin(ctx) if err != nil { log.Errorf("creating transaction: %v", err) - return err + return errors.Wrap(err, "creating transaction") } defer tx.Rollback(ctx) @@ -275,7 +275,7 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error { err = s.DB.SetMemberNamesPronouns(ctx, tx, m.ID, names, pronouns) if err != nil { log.Errorf("setting names for member %v: %v", m.ID, err) - return err + return errors.Wrap(err, "setting names/pronouns") } m.Names = names m.Pronouns = pronouns @@ -286,14 +286,14 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error { err = s.DB.SetMemberFields(ctx, tx, m.ID, *req.Fields) if err != nil { log.Errorf("setting fields for member %v: %v", m.ID, err) - return err + return errors.Wrap(err, "setting fields") } fields = *req.Fields } else { fields, err = s.DB.MemberFields(ctx, m.ID) if err != nil { log.Errorf("getting fields for member %v: %v", m.ID, err) - return err + return errors.Wrap(err, "getting fields") } } @@ -306,7 +306,7 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error { } log.Errorf("updating flags for member %v: %v", m.ID, err) - return err + return errors.Wrap(err, "updating flags") } } @@ -314,20 +314,20 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error { err = s.DB.UpdateActiveTime(ctx, tx, claims.UserID) if err != nil { log.Errorf("updating last active time for user %v: %v", claims.UserID, err) - return err + return errors.Wrap(err, "updating last active time") } err = tx.Commit(ctx) if err != nil { log.Errorf("committing transaction: %v", err) - return err + return errors.Wrap(err, "committing transaction") } // get flags to return (we need to return full flag objects, not the array of IDs in the request body) flags, err := s.DB.MemberFlags(ctx, m.ID) if err != nil { log.Errorf("getting user flags: %v", err) - return err + return errors.Wrap(err, "getting flags") } // echo the updated member back on success diff --git a/backend/routes/v1/user/export.go b/backend/routes/v1/user/export.go index 2dde011..189b3bb 100644 --- a/backend/routes/v1/user/export.go +++ b/backend/routes/v1/user/export.go @@ -7,6 +7,7 @@ import ( "codeberg.org/pronounscc/pronouns.cc/backend/db" "codeberg.org/pronounscc/pronouns.cc/backend/log" "codeberg.org/pronounscc/pronouns.cc/backend/server" + "emperror.dev/errors" "github.com/go-chi/render" ) @@ -71,7 +72,7 @@ func (s *Server) getExport(w http.ResponseWriter, r *http.Request) error { } log.Errorf("getting export for user %v: %v", claims.UserID, err) - return err + return errors.Wrap(err, "getting export") } render.JSON(w, r, dataExportResponse{ diff --git a/backend/routes/v1/user/get_user.go b/backend/routes/v1/user/get_user.go index af43a4b..297c469 100644 --- a/backend/routes/v1/user/get_user.go +++ b/backend/routes/v1/user/get_user.go @@ -8,6 +8,7 @@ import ( "codeberg.org/pronounscc/pronouns.cc/backend/db" "codeberg.org/pronounscc/pronouns.cc/backend/log" "codeberg.org/pronounscc/pronouns.cc/backend/server" + "emperror.dev/errors" "github.com/go-chi/chi/v5" "github.com/go-chi/render" "github.com/rs/xid" @@ -146,7 +147,7 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) (err error) { } } else if err != nil { log.Errorf("Error getting user by username: %v", err) - return err + return errors.Wrap(err, "getting user") } } @@ -162,13 +163,13 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) (err error) { fields, err := s.DB.UserFields(ctx, u.ID) if err != nil { log.Errorf("Error getting user fields: %v", err) - return err + return errors.Wrap(err, "getting fields") } flags, err := s.DB.UserFlags(ctx, u.ID) if err != nil { log.Errorf("getting user flags: %v", err) - return err + return errors.Wrap(err, "getting flags") } var members []db.Member @@ -176,7 +177,7 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) (err error) { members, err = s.DB.UserMembers(ctx, u.ID, isSelf) if err != nil { log.Errorf("Error getting user members: %v", err) - return err + return errors.Wrap(err, "getting user members") } } @@ -191,25 +192,25 @@ func (s *Server) getMeUser(w http.ResponseWriter, r *http.Request) error { u, err := s.DB.User(ctx, claims.UserID) if err != nil { log.Errorf("Error getting user: %v", err) - return err + return errors.Wrap(err, "getting users") } fields, err := s.DB.UserFields(ctx, u.ID) if err != nil { log.Errorf("Error getting user fields: %v", err) - return err + return errors.Wrap(err, "getting fields") } members, err := s.DB.UserMembers(ctx, u.ID, true) if err != nil { log.Errorf("Error getting user members: %v", err) - return err + return errors.Wrap(err, "getting members") } flags, err := s.DB.UserFlags(ctx, u.ID) if err != nil { log.Errorf("getting user flags: %v", err) - return err + return errors.Wrap(err, "getting flags") } render.JSON(w, r, GetMeResponse{ diff --git a/backend/routes/v1/user/patch_user.go b/backend/routes/v1/user/patch_user.go index fc465b1..ca489ce 100644 --- a/backend/routes/v1/user/patch_user.go +++ b/backend/routes/v1/user/patch_user.go @@ -195,13 +195,13 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { } log.Errorf("converting user avatar: %v", err) - return err + return errors.Wrap(err, "converting avatar") } hash, err := s.DB.WriteUserAvatar(ctx, claims.UserID, webp, jpg) if err != nil { log.Errorf("uploading user avatar: %v", err) - return err + return errors.Wrap(err, "uploading avatar") } avatarHash = &hash @@ -219,7 +219,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { tx, err := s.DB.Begin(ctx) if err != nil { log.Errorf("creating transaction: %v", err) - return err + return errors.Wrap(err, "creating transaction") } defer tx.Rollback(ctx) @@ -243,7 +243,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { u, err = s.DB.UpdateUser(ctx, tx, claims.UserID, req.DisplayName, req.Bio, req.MemberTitle, req.ListPrivate, req.Links, avatarHash, req.Timezone, req.CustomPreferences) if err != nil && errors.Cause(err) != db.ErrNothingToUpdate { log.Errorf("updating user: %v", err) - return err + return errors.Wrap(err, "updating user") } if req.Names != nil || req.Pronouns != nil { @@ -260,7 +260,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { err = s.DB.SetUserNamesPronouns(ctx, tx, claims.UserID, names, pronouns) if err != nil { log.Errorf("setting names for member %v: %v", claims.UserID, err) - return err + return errors.Wrap(err, "setting names/pronouns") } u.Names = names u.Pronouns = pronouns @@ -271,14 +271,14 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { err = s.DB.SetUserFields(ctx, tx, claims.UserID, *req.Fields) if err != nil { log.Errorf("setting fields for user %v: %v", claims.UserID, err) - return err + return errors.Wrap(err, "setting fields") } fields = *req.Fields } else { fields, err = s.DB.UserFields(ctx, claims.UserID) if err != nil { log.Errorf("getting fields for user %v: %v", claims.UserID, err) - return err + return errors.Wrap(err, "getting fields") } } @@ -291,7 +291,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { } log.Errorf("updating flags for user %v: %v", claims.UserID, err) - return err + return errors.Wrap(err, "updating flags") } } @@ -299,13 +299,13 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { err = s.DB.UpdateActiveTime(ctx, tx, claims.UserID) if err != nil { log.Errorf("updating last active time for user %v: %v", claims.UserID, err) - return err + return errors.Wrap(err, "updating last active time") } err = tx.Commit(ctx) if err != nil { log.Errorf("committing transaction: %v", err) - return err + return errors.Wrap(err, "committing transaction") } // get fedi instance name if the user has a linked fedi account @@ -321,7 +321,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error { flags, err := s.DB.UserFlags(ctx, u.ID) if err != nil { log.Errorf("getting user flags: %v", err) - return err + return errors.Wrap(err, "getting flags") } // echo the updated user back on success diff --git a/backend/routes/v2/user/get_settings.go b/backend/routes/v2/user/get_settings.go index d618831..096c0e0 100644 --- a/backend/routes/v2/user/get_settings.go +++ b/backend/routes/v2/user/get_settings.go @@ -5,6 +5,7 @@ import ( "codeberg.org/pronounscc/pronouns.cc/backend/log" "codeberg.org/pronounscc/pronouns.cc/backend/server" + "emperror.dev/errors" "github.com/go-chi/render" ) @@ -13,7 +14,7 @@ func (s *Server) GetSettings(w http.ResponseWriter, r *http.Request) (err error) u, err := s.DB.User(r.Context(), claims.UserID) if err != nil { log.Errorf("getting user: %v", err) - return err + return errors.Wrap(err, "getting user") } render.JSON(w, r, u.Settings) diff --git a/backend/server/errors.go b/backend/server/errors.go index b4b8b07..ba33050 100644 --- a/backend/server/errors.go +++ b/backend/server/errors.go @@ -1,10 +1,13 @@ package server import ( + "context" "fmt" "net/http" "codeberg.org/pronounscc/pronouns.cc/backend/log" + "github.com/getsentry/sentry-go" + "github.com/go-chi/chi/v5" "github.com/go-chi/render" ) @@ -12,6 +15,17 @@ import ( // The inner HandlerFunc additionally returns an error. func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + hub := sentry.CurrentHub().Clone() + + defer func(hub *sentry.Hub, r *http.Request) { + if err := recover(); err != nil { + hub.RecoverWithContext( + context.WithValue(r.Context(), sentry.RequestContextKey, r), + err, + ) + } + }(hub, r) + err := hn(w, r) if err != nil { // if the function returned an API error, just render that verbatim @@ -24,10 +38,16 @@ func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.Han return } - // otherwise, we log the error and return an internal server error message - log.Errorf("error in http handler: %v", err) + rctx := chi.RouteContext(r.Context()) + hub.ConfigureScope(func(scope *sentry.Scope) { + scope.SetTag("method", rctx.RouteMethod) + scope.SetTag("path", rctx.RoutePattern()) + }) - apiErr := APIError{Code: ErrInternalServerError} + log.Errorf("error in handler for %v %v: %v", rctx.RouteMethod, rctx.RoutePattern(), err) + + eventID := hub.CaptureException(err) + apiErr := APIError{ID: eventID, Code: ErrInternalServerError} apiErr.prepare() render.Status(r, apiErr.Status) @@ -39,9 +59,10 @@ func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.Han // APIError is an object returned by the API when an error occurs. // It implements the error interface and can be returned by handlers. type APIError struct { - Code int `json:"code"` - Message string `json:"message,omitempty"` - Details string `json:"details,omitempty"` + Code int `json:"code"` + ID *sentry.EventID `json:"id,omitempty"` + Message string `json:"message,omitempty"` + Details string `json:"details,omitempty"` RatelimitReset *int `json:"ratelimit_reset,omitempty"` diff --git a/docs/api/errors.md b/docs/api/errors.md index afb5245..dbb6cc0 100644 --- a/docs/api/errors.md +++ b/docs/api/errors.md @@ -2,12 +2,13 @@ If there is an error in your request, or the server encounters an error while processing it, an error object will be returned. -| Field | Type | Description | -| --------------- | ------- | --------------------------------------------------------------- | -| code | int | an [error code](./errors#error-codes) | -| message | ?string | a human-readable description of the error | -| details | ?string | more details about the error, most often for bad request errors | -| ratelimit_reset | ?int | the unix time when an expired rate limit will reset | +| Field | Type | Description | +| --------------- | ------- | ------------------------------------------------------------------- | +| code | int | an [error code](./errors#error-codes) | +| id | ?string | an opaque Sentry event ID, only returned for internal server errors | +| message | ?string | a human-readable description of the error | +| details | ?string | more details about the error, most often for bad request errors | +| ratelimit_reset | ?int | the unix time when an expired rate limit will reset | ### Error codes diff --git a/go.mod b/go.mod index 360bd54..9a3c02c 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/bwmarrin/discordgo v0.27.1 github.com/davidbyttow/govips/v2 v2.13.0 github.com/georgysavva/scany/v2 v2.0.0 + github.com/getsentry/sentry-go v0.24.1 github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/cors v1.2.1 github.com/go-chi/httprate v0.7.1 diff --git a/go.sum b/go.sum index 3476804..00cfa6f 100644 --- a/go.sum +++ b/go.sum @@ -131,6 +131,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/georgysavva/scany/v2 v2.0.0 h1:RGXqxDv4row7/FYoK8MRXAZXqoWF/NM+NP0q50k3DKU= github.com/georgysavva/scany/v2 v2.0.0/go.mod h1:sigOdh+0qb/+aOs3TVhehVT10p8qJL7K/Zhyz8vWo38= +github.com/getsentry/sentry-go v0.24.1 h1:W6/0GyTy8J6ge6lVCc94WB6Gx2ZuLrgopnn9w8Hiwuk= +github.com/getsentry/sentry-go v0.24.1/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= @@ -140,6 +142,7 @@ github.com/go-chi/httprate v0.7.1 h1:d5kXARdms2PREQfU4pHvq44S6hJ1hPu4OXLeBKmCKWs github.com/go-chi/httprate v0.7.1/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A= github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg= github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -397,6 +400,7 @@ github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -481,8 +485,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tilinna/clock v1.0.2/go.mod h1:ZsP7BcY7sEEz7ktc0IVy8Us6boDrK8VradlKRUGfOao= github.com/tilinna/clock v1.1.0 h1:6IQQQCo6KoBxVudv6gwtY8o4eDfhHo8ojA5dP0MfhSs=