mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 20:56:51 +00:00
feat(BRIDGE-373): extend label conflict resolver logging & report sync errors to sentry
This commit is contained in:
2
go.mod
2
go.mod
@ -7,7 +7,7 @@ toolchain go1.24.2
|
|||||||
require (
|
require (
|
||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||||
github.com/Masterminds/semver/v3 v3.2.0
|
github.com/Masterminds/semver/v3 v3.2.0
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20250528125353-9b611f58b753
|
github.com/ProtonMail/gluon v0.17.1-0.20250603132151-341ea279ce81
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton
|
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -38,6 +38,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
|
|||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20250528125353-9b611f58b753 h1:Zym7WHKLOu1RAUc9b8vkhwEaEU2Gi6MkaurCm7zpK6E=
|
github.com/ProtonMail/gluon v0.17.1-0.20250528125353-9b611f58b753 h1:Zym7WHKLOu1RAUc9b8vkhwEaEU2Gi6MkaurCm7zpK6E=
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20250528125353-9b611f58b753/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
github.com/ProtonMail/gluon v0.17.1-0.20250528125353-9b611f58b753/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||||
|
github.com/ProtonMail/gluon v0.17.1-0.20250603132151-341ea279ce81 h1:p3X/izcQ3VtYI7qF0pmejByXPTRisFkkS+xCxXgeCxs=
|
||||||
|
github.com/ProtonMail/gluon v0.17.1-0.20250603132151-341ea279ce81/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
|
||||||
github.com/ProtonMail/go-crypto v1.1.4-proton h1:KIo9uNlk3vzlwI7o5VjhiEjI4Ld1TDixOMnoNZyfpFE=
|
github.com/ProtonMail/go-crypto v1.1.4-proton h1:KIo9uNlk3vzlwI7o5VjhiEjI4Ld1TDixOMnoNZyfpFE=
|
||||||
github.com/ProtonMail/go-crypto v1.1.4-proton/go.mod h1:zNoyBJW3p/yVWiHNZgfTF9VsjwqYof5YY0M9kt2QaX0=
|
github.com/ProtonMail/go-crypto v1.1.4-proton/go.mod h1:zNoyBJW3p/yVWiHNZgfTF9VsjwqYof5YY0M9kt2QaX0=
|
||||||
|
|||||||
@ -54,6 +54,9 @@ func NewMocks(tb testing.TB, version, minAuto *semver.Version) *Mocks {
|
|||||||
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
|
mocks.Heartbeat.EXPECT().IsTelemetryAvailable(gomock.Any()).AnyTimes()
|
||||||
mocks.Heartbeat.EXPECT().GetHeartbeatPeriodicInterval().AnyTimes().Return(500 * time.Millisecond)
|
mocks.Heartbeat.EXPECT().GetHeartbeatPeriodicInterval().AnyTimes().Return(500 * time.Millisecond)
|
||||||
|
|
||||||
|
// It's called whenever a context is cancelled during sync. We should ought to remove this and make it more granular in the future.
|
||||||
|
mocks.Reporter.EXPECT().ReportMessageWithContext("Failed to sync, will retry later", gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
return mocks
|
return mocks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -287,3 +287,21 @@ func contextToString(context sentry.Context) sentry.Context {
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NullSentryReporter struct{}
|
||||||
|
|
||||||
|
func (n NullSentryReporter) ReportException(any) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NullSentryReporter) ReportMessage(string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NullSentryReporter) ReportMessageWithContext(string, reporter.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NullSentryReporter) ReportExceptionWithContext(any, reporter.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/ProtonMail/gluon/reporter"
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -92,6 +93,7 @@ type labelConflictResolverImpl struct {
|
|||||||
mailboxFetch mailboxFetcherFn
|
mailboxFetch mailboxFetcherFn
|
||||||
client apiClient
|
client apiClient
|
||||||
reporter sentryReporter
|
reporter sentryReporter
|
||||||
|
log *logrus.Entry
|
||||||
}
|
}
|
||||||
|
|
||||||
type nullLabelConflictResolverImpl struct {
|
type nullLabelConflictResolverImpl struct {
|
||||||
@ -112,15 +114,25 @@ func (m *LabelConflictManager) NewConflictResolver(connectors []*Connector) Labe
|
|||||||
mailboxFetch: m.generateMailboxFetcher(connectors),
|
mailboxFetch: m.generateMailboxFetcher(connectors),
|
||||||
client: m.client,
|
client: m.client,
|
||||||
reporter: m.reporter,
|
reporter: m.reporter,
|
||||||
|
log: logrus.WithFields(logrus.Fields{
|
||||||
|
"pkg": "imapservice/labelConflictResolver",
|
||||||
|
"numberOfConnectors": len(connectors),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *labelConflictResolverImpl) ResolveConflict(ctx context.Context, label proton.Label, visited map[string]bool) (func() []imap.Update, error) {
|
func (r *labelConflictResolverImpl) ResolveConflict(ctx context.Context, label proton.Label, visited map[string]bool) (func() []imap.Update, error) {
|
||||||
|
logger := r.log.WithFields(logrus.Fields{
|
||||||
|
"labelID": label.ID,
|
||||||
|
"labelPath": hashLabelPaths(GetMailboxName(label)),
|
||||||
|
})
|
||||||
|
|
||||||
var updateFns []func() []imap.Update
|
var updateFns []func() []imap.Update
|
||||||
|
|
||||||
// There's a cycle, such as in a label swap operation, we'll need to temporarily rename the label.
|
// There's a cycle, such as in a label swap operation, we'll need to temporarily rename the label.
|
||||||
// The change will be overwritten by one of the previous recursive calls.
|
// The change will be overwritten by one of the previous recursive calls.
|
||||||
if visited[label.ID] {
|
if visited[label.ID] {
|
||||||
|
logrus.Info("Cycle detected, applying temporary rename")
|
||||||
fn := func() []imap.Update {
|
fn := func() []imap.Update {
|
||||||
return []imap.Update{newMailboxUpdatedOrCreated(imap.MailboxID(label.ID), getMailboxNameWithTempPrefix(label))}
|
return []imap.Update{newMailboxUpdatedOrCreated(imap.MailboxID(label.ID), getMailboxNameWithTempPrefix(label))}
|
||||||
}
|
}
|
||||||
@ -134,6 +146,7 @@ func (r *labelConflictResolverImpl) ResolveConflict(ctx context.Context, label p
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// Name is free, create the mailbox.
|
// Name is free, create the mailbox.
|
||||||
if db.IsErrNotFound(err) {
|
if db.IsErrNotFound(err) {
|
||||||
|
logger.Info("Label not found in DB, creating mailbox.")
|
||||||
fn := func() []imap.Update {
|
fn := func() []imap.Update {
|
||||||
return []imap.Update{newMailboxUpdatedOrCreated(imap.MailboxID(label.ID), GetMailboxName(label))}
|
return []imap.Update{newMailboxUpdatedOrCreated(imap.MailboxID(label.ID), GetMailboxName(label))}
|
||||||
}
|
}
|
||||||
@ -145,14 +158,23 @@ func (r *labelConflictResolverImpl) ResolveConflict(ctx context.Context, label p
|
|||||||
|
|
||||||
// Verify whether the label name corresponds to the same label ID. If true terminate, we don't need to update.
|
// Verify whether the label name corresponds to the same label ID. If true terminate, we don't need to update.
|
||||||
if mailboxData.RemoteID == label.ID {
|
if mailboxData.RemoteID == label.ID {
|
||||||
|
logger.Info("Mailbox name matches label ID, no conflict.")
|
||||||
return combineIMAPUpdateFns(updateFns), nil
|
return combineIMAPUpdateFns(updateFns), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This means we've found a conflict. So let's log it.
|
||||||
|
logger = logger.WithFields(logrus.Fields{
|
||||||
|
"conflictingLabelID": mailboxData.RemoteID,
|
||||||
|
"conflictingLabelPath": hashLabelPaths(mailboxData.BridgeName),
|
||||||
|
})
|
||||||
|
logger.Info("Label conflict found")
|
||||||
|
|
||||||
// If the label name belongs to some other label ID. Fetch it's state from the remote.
|
// If the label name belongs to some other label ID. Fetch it's state from the remote.
|
||||||
conflictingLabel, err := r.client.GetLabel(ctx, mailboxData.RemoteID, proton.LabelTypeFolder, proton.LabelTypeLabel)
|
conflictingLabel, err := r.client.GetLabel(ctx, mailboxData.RemoteID, proton.LabelTypeFolder, proton.LabelTypeLabel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If it's not present on the remote we should delete it. And create the new label.
|
// If it's not present on the remote we should delete it. And create the new label.
|
||||||
if errors.Is(err, proton.ErrNoSuchLabel) {
|
if errors.Is(err, proton.ErrNoSuchLabel) {
|
||||||
|
logger.Info("Conflicting label does not exist on remote. Deleting.")
|
||||||
fn := func() []imap.Update {
|
fn := func() []imap.Update {
|
||||||
return []imap.Update{
|
return []imap.Update{
|
||||||
imap.NewMailboxDeleted(imap.MailboxID(mailboxData.RemoteID)),
|
imap.NewMailboxDeleted(imap.MailboxID(mailboxData.RemoteID)),
|
||||||
@ -162,6 +184,7 @@ func (r *labelConflictResolverImpl) ResolveConflict(ctx context.Context, label p
|
|||||||
updateFns = append(updateFns, fn)
|
updateFns = append(updateFns, fn)
|
||||||
return combineIMAPUpdateFns(updateFns), nil
|
return combineIMAPUpdateFns(updateFns), nil
|
||||||
}
|
}
|
||||||
|
logger.WithError(err).Error("Failed to fetch conflicting label from remote.")
|
||||||
return combineIMAPUpdateFns(updateFns), err
|
return combineIMAPUpdateFns(updateFns), err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,7 +194,7 @@ func (r *labelConflictResolverImpl) ResolveConflict(ctx context.Context, label p
|
|||||||
"labelID": label.ID,
|
"labelID": label.ID,
|
||||||
"conflictingLabelID": conflictingLabel.ID,
|
"conflictingLabelID": conflictingLabel.ID,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to report update error")
|
logger.WithError(err).Error("Failed to report update error")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := fmt.Errorf("unexpected label conflict: the name of label ID %s is already used by label ID %s", label.ID, conflictingLabel.ID)
|
err := fmt.Errorf("unexpected label conflict: the name of label ID %s is already used by label ID %s", label.ID, conflictingLabel.ID)
|
||||||
@ -180,6 +203,8 @@ func (r *labelConflictResolverImpl) ResolveConflict(ctx context.Context, label p
|
|||||||
|
|
||||||
// The name of the conflicting label has changed on the remote. We need to verify that the new name does not conflict with anything else.
|
// The name of the conflicting label has changed on the remote. We need to verify that the new name does not conflict with anything else.
|
||||||
// Thus, a recursive check can be performed.
|
// Thus, a recursive check can be performed.
|
||||||
|
logger.WithField("conflictingLabelNewPath", hashLabelPaths(conflictingLabel.Path)).
|
||||||
|
Info("Conflicting label name has changed. Recursively resolving conflict.")
|
||||||
childUpdateFns, err := r.ResolveConflict(ctx, conflictingLabel, visited)
|
childUpdateFns, err := r.ResolveConflict(ctx, conflictingLabel, visited)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return combineIMAPUpdateFns(updateFns), err
|
return combineIMAPUpdateFns(updateFns), err
|
||||||
@ -209,3 +234,7 @@ func compareLabelNames(labelName1, labelName2 []string) bool {
|
|||||||
name2 := strings.Join(labelName2, "")
|
name2 := strings.Join(labelName2, "")
|
||||||
return name1 == name2
|
return name1 == name2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hashLabelPaths(path []string) string {
|
||||||
|
return algo.HashBase64SHA256(strings.Join(path, ""))
|
||||||
|
}
|
||||||
|
|||||||
@ -181,7 +181,14 @@ func (s *Service) Start(
|
|||||||
s.syncStateProvider = syncStateProvider
|
s.syncStateProvider = syncStateProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
s.syncHandler = syncservice.NewHandler(syncRegulator, s.client, s.identityState.UserID(), s.syncStateProvider, s.log, s.panicHandler)
|
s.syncHandler = syncservice.NewHandler(
|
||||||
|
syncRegulator,
|
||||||
|
s.client,
|
||||||
|
s.identityState.UserID(),
|
||||||
|
s.syncStateProvider,
|
||||||
|
s.log,
|
||||||
|
s.panicHandler,
|
||||||
|
s.reporter)
|
||||||
|
|
||||||
// Get user labels
|
// Get user labels
|
||||||
apiLabels, err := s.client.GetLabels(ctx, proton.LabelTypeSystem, proton.LabelTypeFolder, proton.LabelTypeLabel)
|
apiLabels, err := s.client.GetLabels(ctx, proton.LabelTypeSystem, proton.LabelTypeFolder, proton.LabelTypeLabel)
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/network"
|
"github.com/ProtonMail/proton-bridge/v3/internal/network"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -45,6 +46,7 @@ type Handler struct {
|
|||||||
syncFinishedCh chan error
|
syncFinishedCh chan error
|
||||||
panicHandler async.PanicHandler
|
panicHandler async.PanicHandler
|
||||||
downloadCache *DownloadCache
|
downloadCache *DownloadCache
|
||||||
|
sentryReporter reporter.Reporter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(
|
func NewHandler(
|
||||||
@ -54,6 +56,7 @@ func NewHandler(
|
|||||||
state StateProvider,
|
state StateProvider,
|
||||||
log *logrus.Entry,
|
log *logrus.Entry,
|
||||||
panicHandler async.PanicHandler,
|
panicHandler async.PanicHandler,
|
||||||
|
sentryReporter reporter.Reporter,
|
||||||
) *Handler {
|
) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
client: client,
|
client: client,
|
||||||
@ -65,6 +68,7 @@ func NewHandler(
|
|||||||
regulator: regulator,
|
regulator: regulator,
|
||||||
panicHandler: panicHandler,
|
panicHandler: panicHandler,
|
||||||
downloadCache: newDownloadCache(),
|
downloadCache: newDownloadCache(),
|
||||||
|
sentryReporter: sentryReporter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,6 +109,14 @@ func (t *Handler) Execute(
|
|||||||
break
|
break
|
||||||
} else if err = t.run(ctx, syncReporter, labels, updateApplier, messageBuilder); err != nil {
|
} else if err = t.run(ctx, syncReporter, labels, updateApplier, messageBuilder); err != nil {
|
||||||
t.log.WithError(err).Error("Failed to sync, will retry later")
|
t.log.WithError(err).Error("Failed to sync, will retry later")
|
||||||
|
|
||||||
|
if sentryErr := t.sentryReporter.ReportMessageWithContext("Failed to sync, will retry later", reporter.Context{
|
||||||
|
"err": err.Error(),
|
||||||
|
"user_id": t.userID,
|
||||||
|
}); sentryErr != nil {
|
||||||
|
t.log.WithError(sentryErr).Error("Failed to report sentry message")
|
||||||
|
}
|
||||||
|
|
||||||
sleepCtx(ctx, coolDown)
|
sleepCtx(ctx, coolDown)
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||||
"github.com/bradenaw/juniper/xmaps"
|
"github.com/bradenaw/juniper/xmaps"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -342,7 +343,7 @@ func newTestHandler(mockCtrl *gomock.Controller, userID string) thandler { // no
|
|||||||
client := NewMockAPIClient(mockCtrl)
|
client := NewMockAPIClient(mockCtrl)
|
||||||
messageBuilder := NewMockMessageBuilder(mockCtrl)
|
messageBuilder := NewMockMessageBuilder(mockCtrl)
|
||||||
syncReporter := NewMockReporter(mockCtrl)
|
syncReporter := NewMockReporter(mockCtrl)
|
||||||
task := NewHandler(regulator, client, userID, syncState, logrus.WithField("test", "test"), &async.NoopPanicHandler{})
|
task := NewHandler(regulator, client, userID, syncState, logrus.WithField("test", "test"), &async.NoopPanicHandler{}, sentry.NullSentryReporter{})
|
||||||
|
|
||||||
return thandler{
|
return thandler{
|
||||||
task: task,
|
task: task,
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/ProtonMail/go-proton-api/server/backend"
|
"github.com/ProtonMail/go-proton-api/server/backend"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
"github.com/ProtonMail/proton-bridge/v3/internal/certs"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/sentry"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/notifications"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/notifications"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
@ -157,7 +158,7 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
|
|||||||
ctx,
|
ctx,
|
||||||
vaultUser,
|
vaultUser,
|
||||||
client,
|
client,
|
||||||
nil,
|
sentry.NullSentryReporter{},
|
||||||
apiUser,
|
apiUser,
|
||||||
nil,
|
nil,
|
||||||
true,
|
true,
|
||||||
|
|||||||
@ -89,8 +89,11 @@ func (r *reportRecorder) close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *reportRecorder) assertEmpty() {
|
func (r *reportRecorder) assertEmpty() {
|
||||||
if !r.skipAssert {
|
if !r.skipAssert && len(r.reports) > 0 {
|
||||||
r.assert.Empty(r.reports)
|
for _, report := range r.reports {
|
||||||
|
// Sentry reports with failed syncs are expected, mostly due to sync context cancellations.
|
||||||
|
r.assert.Equal(report.message, "Failed to sync, will retry later")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user