feat(backend): add sentry integration

This commit is contained in:
sam 2023-09-20 02:39:14 +02:00
parent a6d31d150c
commit b04ed68832
No known key found for this signature in database
GPG Key ID: B4EF20DDE721CAA1
19 changed files with 130 additions and 90 deletions

View File

@ -11,6 +11,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server" "codeberg.org/pronounscc/pronouns.cc/backend/server"
"github.com/davidbyttow/govips/v2/vips" "github.com/davidbyttow/govips/v2/vips"
"github.com/getsentry/sentry-go"
"github.com/go-chi/render" "github.com/go-chi/render"
_ "github.com/joho/godotenv/autoload" _ "github.com/joho/godotenv/autoload"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -23,6 +24,14 @@ var Command = &cli.Command{
} }
func run(c *cli.Context) error { 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 // set vips log level to WARN, else it will spam logs on info level
vips.LoggingSettings(nil, vips.LogLevelWarning) vips.LoggingSettings(nil, vips.LogLevelWarning)

View File

@ -61,7 +61,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
// if the state can't be validated, return // if the state can't be validated, return
if valid, err := s.validateCSRFState(ctx, decoded.State); !valid { if valid, err := s.validateCSRFState(ctx, decoded.State); !valid {
if err != nil { if err != nil {
return err return errors.Wrap(err, "validating state")
} }
return server.APIError{Code: server.ErrInvalidState} 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) dg, _ := discordgo.New(token.Type() + " " + token.AccessToken)
du, err := dg.User("@me") du, err := dg.User("@me")
if err != nil { if err != nil {
return err return errors.Wrap(err, "getting discord user")
} }
u, err := s.DB.DiscordUser(ctx, du.ID) 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) err = s.saveUndeleteToken(ctx, u.ID, token)
if err != nil { if err != nil {
log.Errorf("saving undelete token: %v", err) log.Errorf("saving undelete token: %v", err)
return err return errors.Wrap(err, "saving undelete token")
} }
render.JSON(w, r, discordCallbackResponse{ render.JSON(w, r, discordCallbackResponse{
@ -114,7 +114,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
tokenID := xid.New() tokenID := xid.New()
token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true) token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true)
if err != nil { if err != nil {
return err return errors.Wrap(err, "creating token")
} }
// save token to database // save token to database
@ -137,7 +137,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
return nil return nil
} else if err != db.ErrUserNotFound { // internal error } 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 // 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") err = s.DB.SetJSON(ctx, "discord:"+ticket, du, "EX", "600")
if err != nil { if err != nil {
log.Errorf("setting Discord user for ticket %q: %v", ticket, err) 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{ 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) valid, taken, err := s.DB.UsernameTaken(ctx, req.Username)
if err != nil { if err != nil {
return err return errors.Wrap(err, "checking if username is taken")
} }
if !valid { if !valid {
return server.APIError{Code: server.ErrInvalidUsername} return server.APIError{Code: server.ErrInvalidUsername}

View File

@ -54,7 +54,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
// if the state can't be validated, return // if the state can't be validated, return
if valid, err := s.validateCSRFState(ctx, decoded.State); !valid { if valid, err := s.validateCSRFState(ctx, decoded.State); !valid {
if err != nil { if err != nil {
return err return errors.Wrap(err, "validating state")
} }
return server.APIError{Code: server.ErrInvalidState} 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) err = s.saveUndeleteToken(ctx, u.ID, token)
if err != nil { if err != nil {
log.Errorf("saving undelete token: %v", err) log.Errorf("saving undelete token: %v", err)
return err return errors.Wrap(err, "saving undelete token")
} }
render.JSON(w, r, fediCallbackResponse{ render.JSON(w, r, fediCallbackResponse{
@ -135,7 +135,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
tokenID := xid.New() tokenID := xid.New()
token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true) token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true)
if err != nil { if err != nil {
return err return errors.Wrap(err, "creating token")
} }
// save token to database // save token to database
@ -158,7 +158,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
return nil return nil
} else if err != db.ErrUserNotFound { // internal error } 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 // 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") err = s.DB.SetJSON(ctx, "mastodon:"+ticket, mu, "EX", "600")
if err != nil { if err != nil {
log.Errorf("setting mastoAPI user for ticket %q: %v", ticket, err) 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{ 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) valid, taken, err := s.DB.UsernameTaken(ctx, req.Username)
if err != nil { if err != nil {
return err return errors.Wrap(err, "checking if username is taken")
} }
if !valid { if !valid {
return server.APIError{Code: server.ErrInvalidUsername} return server.APIError{Code: server.ErrInvalidUsername}

View File

@ -90,7 +90,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
err = s.saveUndeleteToken(ctx, u.ID, token) err = s.saveUndeleteToken(ctx, u.ID, token)
if err != nil { if err != nil {
log.Errorf("saving undelete token: %v", err) log.Errorf("saving undelete token: %v", err)
return err return errors.Wrap(err, "saving undelete token")
} }
render.JSON(w, r, fediCallbackResponse{ render.JSON(w, r, fediCallbackResponse{
@ -114,7 +114,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
tokenID := xid.New() tokenID := xid.New()
token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true) token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true)
if err != nil { if err != nil {
return err return errors.Wrap(err, "creating token")
} }
// save token to database // save token to database
@ -137,7 +137,7 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
return nil return nil
} else if err != db.ErrUserNotFound { // internal error } 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 // 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") err = s.DB.SetJSON(ctx, "misskey:"+ticket, mu.User, "EX", "600")
if err != nil { if err != nil {
log.Errorf("setting misskey user for ticket %q: %v", ticket, err) 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{ 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) valid, taken, err := s.DB.UsernameTaken(ctx, req.Username)
if err != nil { if err != nil {
return err return errors.Wrap(err, "checking if username is taken")
} }
if !valid { if !valid {
return server.APIError{Code: server.ErrInvalidUsername} return server.APIError{Code: server.ErrInvalidUsername}

View File

@ -60,7 +60,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
// if the state can't be validated, return // if the state can't be validated, return
if valid, err := s.validateCSRFState(ctx, decoded.State); !valid { if valid, err := s.validateCSRFState(ctx, decoded.State); !valid {
if err != nil { if err != nil {
return err return errors.Wrap(err, "validating state")
} }
return server.APIError{Code: server.ErrInvalidState} 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) err = s.saveUndeleteToken(ctx, u.ID, token)
if err != nil { if err != nil {
log.Errorf("saving undelete token: %v", err) log.Errorf("saving undelete token: %v", err)
return err return errors.Wrap(err, "saving undelete token")
} }
render.JSON(w, r, googleCallbackResponse{ render.JSON(w, r, googleCallbackResponse{
@ -133,7 +133,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
tokenID := xid.New() tokenID := xid.New()
token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true) token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true)
if err != nil { if err != nil {
return err return errors.Wrap(err, "creating token")
} }
// save token to database // save token to database
@ -156,7 +156,7 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
return nil return nil
} else if err != db.ErrUserNotFound { // internal error } 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 // 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") err = s.DB.SetJSON(ctx, "google:"+ticket, partialGoogleUser{ID: googleID, Email: googleUsername}, "EX", "600")
if err != nil { if err != nil {
log.Errorf("setting Google user for ticket %q: %v", ticket, err) 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{ 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) valid, taken, err := s.DB.UsernameTaken(ctx, req.Username)
if err != nil { if err != nil {
return err return errors.Wrap(err, "checking if username is taken")
} }
if !valid { if !valid {
return server.APIError{Code: server.ErrInvalidUsername} return server.APIError{Code: server.ErrInvalidUsername}

View File

@ -77,7 +77,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
// if the state can't be validated, return // if the state can't be validated, return
if valid, err := s.validateCSRFState(ctx, decoded.State); !valid { if valid, err := s.validateCSRFState(ctx, decoded.State); !valid {
if err != nil { if err != nil {
return err return errors.Wrap(err, "validating state")
} }
return server.APIError{Code: server.ErrInvalidState} 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) err = s.saveUndeleteToken(ctx, u.ID, token)
if err != nil { if err != nil {
log.Errorf("saving undelete token: %v", err) log.Errorf("saving undelete token: %v", err)
return err return errors.Wrap(err, "saving undelete token")
} }
render.JSON(w, r, tumblrCallbackResponse{ render.JSON(w, r, tumblrCallbackResponse{
@ -166,7 +166,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
tokenID := xid.New() tokenID := xid.New()
token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true) token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true)
if err != nil { if err != nil {
return err return errors.Wrap(err, "creating token")
} }
// save token to database // save token to database
@ -189,7 +189,7 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
return nil return nil
} else if err != db.ErrUserNotFound { // internal error } 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 // 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") err = s.DB.SetJSON(ctx, "tumblr:"+ticket, tumblrUserInfo{ID: tumblrID, Name: tumblrName}, "EX", "600")
if err != nil { if err != nil {
log.Errorf("setting Tumblr user for ticket %q: %v", ticket, err) 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{ 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) valid, taken, err := s.DB.UsernameTaken(ctx, req.Username)
if err != nil { if err != nil {
return err return errors.Wrap(err, "checking if username is taken")
} }
if !valid { if !valid {
return server.APIError{Code: server.ErrInvalidUsername} return server.APIError{Code: server.ErrInvalidUsername}

View File

@ -127,14 +127,14 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error
return server.APIError{Code: server.ErrMemberNameInUse} return server.APIError{Code: server.ErrMemberNameInUse}
} }
return err return errors.Wrap(err, "creating member")
} }
// set names, pronouns, fields // set names, pronouns, fields
err = s.DB.SetMemberNamesPronouns(ctx, tx, m.ID, db.NotNull(cmr.Names), db.NotNull(cmr.Pronouns)) err = s.DB.SetMemberNamesPronouns(ctx, tx, m.ID, db.NotNull(cmr.Names), db.NotNull(cmr.Pronouns))
if err != nil { if err != nil {
log.Errorf("setting names and pronouns for member %v: %v", m.ID, err) 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.Names = cmr.Names
m.Pronouns = cmr.Pronouns 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) err = s.DB.SetMemberFields(ctx, tx, m.ID, cmr.Fields)
if err != nil { if err != nil {
log.Errorf("setting fields for member %v: %v", m.ID, err) log.Errorf("setting fields for member %v: %v", m.ID, err)
return err return errors.Wrap(err, "setting fields")
} }
if cmr.Avatar != "" { 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) 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) hash, err := s.DB.WriteMemberAvatar(ctx, m.ID, webp, jpg)
if err != nil { if err != nil {
log.Errorf("uploading member avatar: %v", err) 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) 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) err = s.DB.UpdateActiveTime(ctx, tx, claims.UserID)
if err != nil { if err != nil {
log.Errorf("updating last active time for user %v: %v", claims.UserID, err) 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) err = tx.Commit(ctx)

View File

@ -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) err = s.DB.UpdateActiveTime(ctx, s.DB, claims.UserID)
if err != nil { if err != nil {
log.Errorf("updating last active time for user %v: %v", claims.UserID, err) 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) render.NoContent(w, r)

View File

@ -105,7 +105,7 @@ func (s *Server) getMember(w http.ResponseWriter, r *http.Request) (err error) {
u, err := s.DB.User(ctx, m.UserID) u, err := s.DB.User(ctx, m.UserID)
if err != nil { if err != nil {
return err return errors.Wrap(err, "getting user")
} }
if u.DeletedAt != nil { 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) fields, err := s.DB.MemberFields(ctx, m.ID)
if err != nil { if err != nil {
return err return errors.Wrap(err, "getting member fields")
} }
flags, err := s.DB.MemberFlags(ctx, m.ID) flags, err := s.DB.MemberFlags(ctx, m.ID)
if err != nil { if err != nil {
return err return errors.Wrap(err, "getting member flags")
} }
render.JSON(w, r, dbMemberToMember(u, m, fields, flags, isOwnMember)) 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) fields, err := s.DB.MemberFields(ctx, m.ID)
if err != nil { if err != nil {
return err return errors.Wrap(err, "getting member fields")
} }
flags, err := s.DB.MemberFlags(ctx, m.ID) flags, err := s.DB.MemberFlags(ctx, m.ID)
if err != nil { if err != nil {
return err return errors.Wrap(err, "getting member flags")
} }
render.JSON(w, r, dbMemberToMember(u, m, fields, flags, isOwnMember)) 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) fields, err := s.DB.MemberFields(ctx, m.ID)
if err != nil { if err != nil {
return err return errors.Wrap(err, "getting member fields")
} }
flags, err := s.DB.MemberFlags(ctx, m.ID) flags, err := s.DB.MemberFlags(ctx, m.ID)
if err != nil { if err != nil {
return err return errors.Wrap(err, "getting member flags")
} }
render.JSON(w, r, dbMemberToMember(u, m, fields, flags, true)) render.JSON(w, r, dbMemberToMember(u, m, fields, flags, true))

View File

@ -6,6 +6,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/common" "codeberg.org/pronounscc/pronouns.cc/backend/common"
"codeberg.org/pronounscc/pronouns.cc/backend/db" "codeberg.org/pronounscc/pronouns.cc/backend/db"
"codeberg.org/pronounscc/pronouns.cc/backend/server" "codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/rs/xid" "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) ms, err := s.DB.UserMembers(ctx, u.ID, isSelf)
if err != nil { if err != nil {
return err return errors.Wrap(err, "getting members")
} }
render.JSON(w, r, membersToMemberList(ms, isSelf)) 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) ms, err := s.DB.UserMembers(ctx, claims.UserID, true)
if err != nil { if err != nil {
return err return errors.Wrap(err, "getting members")
} }
render.JSON(w, r, membersToMemberList(ms, true)) render.JSON(w, r, membersToMemberList(ms, true))

View File

@ -220,13 +220,13 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
} }
log.Errorf("converting member avatar: %v", err) 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) hash, err := s.DB.WriteMemberAvatar(ctx, m.ID, webp, jpg)
if err != nil { if err != nil {
log.Errorf("uploading member avatar: %v", err) log.Errorf("uploading member avatar: %v", err)
return err return errors.Wrap(err, "writing member avatar")
} }
avatarHash = &hash avatarHash = &hash
@ -244,7 +244,7 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
tx, err := s.DB.Begin(ctx) tx, err := s.DB.Begin(ctx)
if err != nil { if err != nil {
log.Errorf("creating transaction: %v", err) log.Errorf("creating transaction: %v", err)
return err return errors.Wrap(err, "creating transaction")
} }
defer tx.Rollback(ctx) 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) err = s.DB.SetMemberNamesPronouns(ctx, tx, m.ID, names, pronouns)
if err != nil { if err != nil {
log.Errorf("setting names for member %v: %v", m.ID, err) log.Errorf("setting names for member %v: %v", m.ID, err)
return err return errors.Wrap(err, "setting names/pronouns")
} }
m.Names = names m.Names = names
m.Pronouns = pronouns 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) err = s.DB.SetMemberFields(ctx, tx, m.ID, *req.Fields)
if err != nil { if err != nil {
log.Errorf("setting fields for member %v: %v", m.ID, err) log.Errorf("setting fields for member %v: %v", m.ID, err)
return err return errors.Wrap(err, "setting fields")
} }
fields = *req.Fields fields = *req.Fields
} else { } else {
fields, err = s.DB.MemberFields(ctx, m.ID) fields, err = s.DB.MemberFields(ctx, m.ID)
if err != nil { if err != nil {
log.Errorf("getting fields for member %v: %v", m.ID, err) 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) 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) err = s.DB.UpdateActiveTime(ctx, tx, claims.UserID)
if err != nil { if err != nil {
log.Errorf("updating last active time for user %v: %v", claims.UserID, err) 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) err = tx.Commit(ctx)
if err != nil { if err != nil {
log.Errorf("committing transaction: %v", err) 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) // 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) flags, err := s.DB.MemberFlags(ctx, m.ID)
if err != nil { if err != nil {
log.Errorf("getting user flags: %v", err) log.Errorf("getting user flags: %v", err)
return err return errors.Wrap(err, "getting flags")
} }
// echo the updated member back on success // echo the updated member back on success

View File

@ -7,6 +7,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/db" "codeberg.org/pronounscc/pronouns.cc/backend/db"
"codeberg.org/pronounscc/pronouns.cc/backend/log" "codeberg.org/pronounscc/pronouns.cc/backend/log"
"codeberg.org/pronounscc/pronouns.cc/backend/server" "codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render" "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) log.Errorf("getting export for user %v: %v", claims.UserID, err)
return err return errors.Wrap(err, "getting export")
} }
render.JSON(w, r, dataExportResponse{ render.JSON(w, r, dataExportResponse{

View File

@ -8,6 +8,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/db" "codeberg.org/pronounscc/pronouns.cc/backend/db"
"codeberg.org/pronounscc/pronouns.cc/backend/log" "codeberg.org/pronounscc/pronouns.cc/backend/log"
"codeberg.org/pronounscc/pronouns.cc/backend/server" "codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/rs/xid" "github.com/rs/xid"
@ -146,7 +147,7 @@ func (s *Server) getUser(w http.ResponseWriter, r *http.Request) (err error) {
} }
} else if err != nil { } else if err != nil {
log.Errorf("Error getting user by username: %v", err) 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) fields, err := s.DB.UserFields(ctx, u.ID)
if err != nil { if err != nil {
log.Errorf("Error getting user fields: %v", err) log.Errorf("Error getting user fields: %v", err)
return err return errors.Wrap(err, "getting fields")
} }
flags, err := s.DB.UserFlags(ctx, u.ID) flags, err := s.DB.UserFlags(ctx, u.ID)
if err != nil { if err != nil {
log.Errorf("getting user flags: %v", err) log.Errorf("getting user flags: %v", err)
return err return errors.Wrap(err, "getting flags")
} }
var members []db.Member 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) members, err = s.DB.UserMembers(ctx, u.ID, isSelf)
if err != nil { if err != nil {
log.Errorf("Error getting user members: %v", err) 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) u, err := s.DB.User(ctx, claims.UserID)
if err != nil { if err != nil {
log.Errorf("Error getting user: %v", err) log.Errorf("Error getting user: %v", err)
return err return errors.Wrap(err, "getting users")
} }
fields, err := s.DB.UserFields(ctx, u.ID) fields, err := s.DB.UserFields(ctx, u.ID)
if err != nil { if err != nil {
log.Errorf("Error getting user fields: %v", err) 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) members, err := s.DB.UserMembers(ctx, u.ID, true)
if err != nil { if err != nil {
log.Errorf("Error getting user members: %v", err) log.Errorf("Error getting user members: %v", err)
return err return errors.Wrap(err, "getting members")
} }
flags, err := s.DB.UserFlags(ctx, u.ID) flags, err := s.DB.UserFlags(ctx, u.ID)
if err != nil { if err != nil {
log.Errorf("getting user flags: %v", err) log.Errorf("getting user flags: %v", err)
return err return errors.Wrap(err, "getting flags")
} }
render.JSON(w, r, GetMeResponse{ render.JSON(w, r, GetMeResponse{

View File

@ -195,13 +195,13 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
} }
log.Errorf("converting user avatar: %v", err) 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) hash, err := s.DB.WriteUserAvatar(ctx, claims.UserID, webp, jpg)
if err != nil { if err != nil {
log.Errorf("uploading user avatar: %v", err) log.Errorf("uploading user avatar: %v", err)
return err return errors.Wrap(err, "uploading avatar")
} }
avatarHash = &hash avatarHash = &hash
@ -219,7 +219,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
tx, err := s.DB.Begin(ctx) tx, err := s.DB.Begin(ctx)
if err != nil { if err != nil {
log.Errorf("creating transaction: %v", err) log.Errorf("creating transaction: %v", err)
return err return errors.Wrap(err, "creating transaction")
} }
defer tx.Rollback(ctx) 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) 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 { if err != nil && errors.Cause(err) != db.ErrNothingToUpdate {
log.Errorf("updating user: %v", err) log.Errorf("updating user: %v", err)
return err return errors.Wrap(err, "updating user")
} }
if req.Names != nil || req.Pronouns != nil { 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) err = s.DB.SetUserNamesPronouns(ctx, tx, claims.UserID, names, pronouns)
if err != nil { if err != nil {
log.Errorf("setting names for member %v: %v", claims.UserID, err) log.Errorf("setting names for member %v: %v", claims.UserID, err)
return err return errors.Wrap(err, "setting names/pronouns")
} }
u.Names = names u.Names = names
u.Pronouns = pronouns 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) err = s.DB.SetUserFields(ctx, tx, claims.UserID, *req.Fields)
if err != nil { if err != nil {
log.Errorf("setting fields for user %v: %v", claims.UserID, err) log.Errorf("setting fields for user %v: %v", claims.UserID, err)
return err return errors.Wrap(err, "setting fields")
} }
fields = *req.Fields fields = *req.Fields
} else { } else {
fields, err = s.DB.UserFields(ctx, claims.UserID) fields, err = s.DB.UserFields(ctx, claims.UserID)
if err != nil { if err != nil {
log.Errorf("getting fields for user %v: %v", claims.UserID, err) 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) 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) err = s.DB.UpdateActiveTime(ctx, tx, claims.UserID)
if err != nil { if err != nil {
log.Errorf("updating last active time for user %v: %v", claims.UserID, err) 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) err = tx.Commit(ctx)
if err != nil { if err != nil {
log.Errorf("committing transaction: %v", err) 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 // 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) flags, err := s.DB.UserFlags(ctx, u.ID)
if err != nil { if err != nil {
log.Errorf("getting user flags: %v", err) log.Errorf("getting user flags: %v", err)
return err return errors.Wrap(err, "getting flags")
} }
// echo the updated user back on success // echo the updated user back on success

View File

@ -5,6 +5,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/log" "codeberg.org/pronounscc/pronouns.cc/backend/log"
"codeberg.org/pronounscc/pronouns.cc/backend/server" "codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render" "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) u, err := s.DB.User(r.Context(), claims.UserID)
if err != nil { if err != nil {
log.Errorf("getting user: %v", err) log.Errorf("getting user: %v", err)
return err return errors.Wrap(err, "getting user")
} }
render.JSON(w, r, u.Settings) render.JSON(w, r, u.Settings)

View File

@ -1,10 +1,13 @@
package server package server
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"codeberg.org/pronounscc/pronouns.cc/backend/log" "codeberg.org/pronounscc/pronouns.cc/backend/log"
"github.com/getsentry/sentry-go"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render" "github.com/go-chi/render"
) )
@ -12,6 +15,17 @@ import (
// The inner HandlerFunc additionally returns an error. // The inner HandlerFunc additionally returns an error.
func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc { func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { 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) err := hn(w, r)
if err != nil { if err != nil {
// if the function returned an API error, just render that verbatim // 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 return
} }
// otherwise, we log the error and return an internal server error message rctx := chi.RouteContext(r.Context())
log.Errorf("error in http handler: %v", err) 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() apiErr.prepare()
render.Status(r, apiErr.Status) 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. // APIError is an object returned by the API when an error occurs.
// It implements the error interface and can be returned by handlers. // It implements the error interface and can be returned by handlers.
type APIError struct { type APIError struct {
Code int `json:"code"` Code int `json:"code"`
Message string `json:"message,omitempty"` ID *sentry.EventID `json:"id,omitempty"`
Details string `json:"details,omitempty"` Message string `json:"message,omitempty"`
Details string `json:"details,omitempty"`
RatelimitReset *int `json:"ratelimit_reset,omitempty"` RatelimitReset *int `json:"ratelimit_reset,omitempty"`

View File

@ -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. 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 | | Field | Type | Description |
| --------------- | ------- | --------------------------------------------------------------- | | --------------- | ------- | ------------------------------------------------------------------- |
| code | int | an [error code](./errors#error-codes) | | code | int | an [error code](./errors#error-codes) |
| message | ?string | a human-readable description of the error | | id | ?string | an opaque Sentry event ID, only returned for internal server errors |
| details | ?string | more details about the error, most often for bad request errors | | message | ?string | a human-readable description of the error |
| ratelimit_reset | ?int | the unix time when an expired rate limit will reset | | 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 ### Error codes

1
go.mod
View File

@ -9,6 +9,7 @@ require (
github.com/bwmarrin/discordgo v0.27.1 github.com/bwmarrin/discordgo v0.27.1
github.com/davidbyttow/govips/v2 v2.13.0 github.com/davidbyttow/govips/v2 v2.13.0
github.com/georgysavva/scany/v2 v2.0.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/chi/v5 v5.0.8
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/go-chi/httprate v0.7.1 github.com/go-chi/httprate v0.7.1

6
go.sum
View File

@ -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/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 h1:RGXqxDv4row7/FYoK8MRXAZXqoWF/NM+NP0q50k3DKU=
github.com/georgysavva/scany/v2 v2.0.0/go.mod h1:sigOdh+0qb/+aOs3TVhehVT10p8qJL7K/Zhyz8vWo38= 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/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 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 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/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 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg=
github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= 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 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-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/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/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.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 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/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.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/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.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.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.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.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/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.0.2/go.mod h1:ZsP7BcY7sEEz7ktc0IVy8Us6boDrK8VradlKRUGfOao=
github.com/tilinna/clock v1.1.0 h1:6IQQQCo6KoBxVudv6gwtY8o4eDfhHo8ojA5dP0MfhSs= github.com/tilinna/clock v1.1.0 h1:6IQQQCo6KoBxVudv6gwtY8o4eDfhHo8ojA5dP0MfhSs=