feat(migration): notify the user when a migration failed
continuous-integration/drone/push Build is failing Details

This change introduces notifications via mail when a migration fails. It will contain the error message and a hint to post it in the forum when Sentry is disabled, otherwise the error message will be sent directly to sentry and the notification will inform accordingly.
I've tried to balance "this thing failed, go figure it out" with "here is what we know and how you can get help", we'll see how well that approach works.
This commit is contained in:
kolaente 2024-04-08 12:15:05 +02:00
parent 61322d2e2e
commit e10cd368bf
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
2 changed files with 105 additions and 4 deletions

View File

@ -17,12 +17,14 @@
package handler
import (
"encoding/json"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/modules/migration"
"code.vikunja.io/api/pkg/notifications"
"encoding/json"
"fmt"
"github.com/getsentry/sentry-go"
"github.com/ThreeDotsLabs/watermill/message"
)
@ -31,6 +33,16 @@ func RegisterListeners() {
events.RegisterListener((&MigrationRequestedEvent{}).Name(), &MigrationListener{})
}
// Only used for sentry
type migrationFailedError struct {
MigratorKind string
OriginalError error
}
func (m *migrationFailedError) Error() string {
return fmt.Sprintf("migration from %s failed, original error message was: %s", m.MigratorKind, m.OriginalError.Error())
}
// MigrationListener represents a listener
type MigrationListener struct {
}
@ -59,13 +71,46 @@ func (s *MigrationListener) Handle(msg *message.Message) (err error) {
ms := event.Migrator.(migration.Migrator)
m, err := migration.StartMigration(ms, event.User)
m, err := migrateInListener(ms, event)
if err != nil {
log.Errorf("[Migration] Migration %d from %s for user %d failed. Error was: %s", m.ID, event.MigratorKind, event.User.ID, err.Error())
var nerr error
if config.SentryEnabled.GetBool() {
nerr = notifications.Notify(event.User, &MigrationFailedReportedNotification{
MigratorName: ms.Name(),
})
sentry.CaptureException(&migrationFailedError{
MigratorKind: event.MigratorKind,
OriginalError: err,
})
} else {
nerr = notifications.Notify(event.User, &MigrationFailedNotification{
MigratorName: ms.Name(),
Error: err,
})
}
if nerr != nil {
log.Errorf("[Migration] Could not sent failed migration notification for migration %d to user %d, error was: %s", m.ID, event.User.ID, err.Error())
}
// Still need to finish the migration, otherwise restarting will not work
err = migration.FinishMigration(m)
if err != nil {
log.Errorf("[Migration] Could not finish migration %d for user %d, error was: %s", m.ID, event.User.ID, err.Error())
}
}
return nil // We do not want the queue to restart this job as we've already handled the error.
}
func migrateInListener(ms migration.Migrator, event *MigrationRequestedEvent) (m *migration.Status, err error) {
m, err = migration.StartMigration(ms, event.User)
if err != nil {
return
}
log.Debugf("[Migration] Starting migration %d from %s for user %d", m.ID, event.MigratorKind, event.User.ID)
err = ms.Migrate(event.User)
if err != nil {
return
@ -73,6 +118,7 @@ func (s *MigrationListener) Handle(msg *message.Message) (err error) {
err = migration.FinishMigration(m)
if err != nil {
log.Errorf("[Migration] Could not finish migration %d for user %d, error was: %s", m.ID, event.User.ID, err.Error())
return
}
@ -80,6 +126,7 @@ func (s *MigrationListener) Handle(msg *message.Message) (err error) {
MigratorName: ms.Name(),
})
if err != nil {
log.Errorf("[Migration] Could not sent migration success notification for migration %d to user %d, error was: %s", m.ID, event.User.ID, err.Error())
return
}

View File

@ -49,3 +49,57 @@ func (n *MigrationDoneNotification) ToDB() interface{} {
func (n *MigrationDoneNotification) Name() string {
return "migration.done"
}
// MigrationFailedReportedNotification represents a MigrationFailedReportedNotification notification
type MigrationFailedReportedNotification struct {
MigratorName string
}
// ToMail returns the mail notification for MigrationFailedReportedNotification
func (n *MigrationFailedReportedNotification) ToMail() *notifications.Mail {
kind := cases.Title(language.English).String(n.MigratorName)
return notifications.NewMail().
Subject("The migration from " + kind + " to Vikunja was has failed").
Line("Looks like the move from " + kind + " didn't go as planned this time.").
Line("No worries, though! Just give it another shot by starting over the same way you did before. Sometimes, these hiccups happen because of glitches on " + kind + "'s end, but trying again often does the trick.").
Line("We've got the error message on our radar and are on it to get it sorted out soon.")
}
// ToDB returns the MigrationFailedReportedNotification notification in a format which can be saved in the db
func (n *MigrationFailedReportedNotification) ToDB() interface{} {
return nil
}
// Name returns the name of the notification
func (n *MigrationFailedReportedNotification) Name() string {
return "migration.failed.reported"
}
// MigrationFailedNotification represents a MigrationFailedNotification notification
type MigrationFailedNotification struct {
MigratorName string
Error error
}
// ToMail returns the mail notification for MigrationFailedNotification
func (n *MigrationFailedNotification) ToMail() *notifications.Mail {
kind := cases.Title(language.English).String(n.MigratorName)
return notifications.NewMail().
Subject("The migration from " + kind + " to Vikunja was has failed").
Line("Looks like the move from " + kind + " didn't go as planned this time.").
Line("No worries, though! Just give it another shot by starting over the same way you did before. Sometimes, these hiccups happen because of glitches on " + kind + "'s end, but trying again often does the trick.").
Line("We bumped into a little error along the way: `" + n.Error.Error() + "`.").
Line("Please drop us a note about this [in the forum](https://community.vikunja.io/) or any of the usual places so that we can take a look at why it failed.")
}
// ToDB returns the MigrationFailedNotification notification in a format which can be saved in the db
func (n *MigrationFailedNotification) ToDB() interface{} {
return nil
}
// Name returns the name of the notification
func (n *MigrationFailedNotification) Name() string {
return "migration.failed"
}