From e10cd368bf5a167d5eb01edec854c901fbb1b806 Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 8 Apr 2024 12:15:05 +0200 Subject: [PATCH] feat(migration): notify the user when a migration failed 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. --- pkg/modules/migration/handler/listeners.go | 55 +++++++++++++++++-- .../migration/handler/notifications.go | 54 ++++++++++++++++++ 2 files changed, 105 insertions(+), 4 deletions(-) diff --git a/pkg/modules/migration/handler/listeners.go b/pkg/modules/migration/handler/listeners.go index e24c89ccb..8a52e319f 100644 --- a/pkg/modules/migration/handler/listeners.go +++ b/pkg/modules/migration/handler/listeners.go @@ -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 } diff --git a/pkg/modules/migration/handler/notifications.go b/pkg/modules/migration/handler/notifications.go index 4768b0ffd..defd2e5c5 100644 --- a/pkg/modules/migration/handler/notifications.go +++ b/pkg/modules/migration/handler/notifications.go @@ -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" +}