mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-17 15:46:44 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6105f32c75 | |||
| f5bc6ad1f0 | |||
| e8a95e26f6 | |||
| ebe54ca92e | |||
| ff7e45f395 | |||
| 79c63f5785 | |||
| 3ca9e625f5 | |||
| 5b874657cb | |||
| bfe67f3005 | |||
| 99e6f00aaa |
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,6 +7,7 @@
|
|||||||
*~
|
*~
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
.vs
|
||||||
|
|
||||||
# Test files
|
# Test files
|
||||||
godog.test
|
godog.test
|
||||||
@ -35,6 +36,8 @@ cmd/Import-Export/deploy
|
|||||||
proton-bridge
|
proton-bridge
|
||||||
cmd/Desktop-Bridge/*.exe
|
cmd/Desktop-Bridge/*.exe
|
||||||
cmd/launcher/*.exe
|
cmd/launcher/*.exe
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
|
||||||
# Jetbrains (CLion, Golang) cmake build dirs
|
# Jetbrains (CLion, Golang) cmake build dirs
|
||||||
cmake-build-*/
|
cmake-build-*/
|
||||||
|
|||||||
13
Changelog.md
13
Changelog.md
@ -3,6 +3,19 @@
|
|||||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||||
|
|
||||||
|
|
||||||
|
## Dragon Bridge 3.14.0
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* BRIDGE-207: Failure to download or verify an update now fails silently.
|
||||||
|
* BRIDGE-204: Removed redundant Sentry events.
|
||||||
|
* BRIDGE-150: Observability service modification.
|
||||||
|
* BRIDGE-210: Reduced log level of cache events so they won't be printed to stdout.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* BRIDGE-106: Fixed import of multipart-related messages.
|
||||||
|
* BRIDGE-108: Fixed GetInitials when empty username is passed.
|
||||||
|
|
||||||
|
|
||||||
## Colorado Bridge 3.13.0
|
## Colorado Bridge 3.13.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
2
Makefile
2
Makefile
@ -12,7 +12,7 @@ ROOT_DIR:=$(realpath .)
|
|||||||
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
.PHONY: build build-gui build-nogui build-launcher versioner hasher
|
||||||
|
|
||||||
# Keep version hardcoded so app build works also without Git repository.
|
# Keep version hardcoded so app build works also without Git repository.
|
||||||
BRIDGE_APP_VERSION?=3.13.0+git
|
BRIDGE_APP_VERSION?=3.14.0+git
|
||||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||||
APP_FULL_NAME:=Proton Mail Bridge
|
APP_FULL_NAME:=Proton Mail Bridge
|
||||||
APP_VENDOR:=Proton AG
|
APP_VENDOR:=Proton AG
|
||||||
|
|||||||
6
go.mod
6
go.mod
@ -7,9 +7,9 @@ toolchain go1.21.9
|
|||||||
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.20240514133734-79cdd0fec41c
|
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602
|
||||||
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.20240829112804-d663a2ef90c2
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47
|
||||||
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton
|
||||||
github.com/PuerkitoBio/goquery v1.8.1
|
github.com/PuerkitoBio/goquery v1.8.1
|
||||||
github.com/abiosoft/ishell v2.0.0+incompatible
|
github.com/abiosoft/ishell v2.0.0+incompatible
|
||||||
@ -122,7 +122,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7
|
github.com/emersion/go-message => github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423
|
||||||
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
|
github.com/emersion/go-smtp => github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865
|
||||||
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a
|
github.com/go-resty/resty/v2 => github.com/LBeernaertProton/resty/v2 v2.0.0-20231129100320-dddf8030d93a
|
||||||
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20240103134243-0b6a41580b77
|
github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20240103134243-0b6a41580b77
|
||||||
|
|||||||
18
go.sum
18
go.sum
@ -29,6 +29,12 @@ 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.20240514133734-79cdd0fec41c h1:P3SvCACt13Zqdj0IRDB4bgwqI68+oMB2j0uVuPQyoTw=
|
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c h1:P3SvCACt13Zqdj0IRDB4bgwqI68+oMB2j0uVuPQyoTw=
|
||||||
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
github.com/ProtonMail/gluon v0.17.1-0.20240514133734-79cdd0fec41c/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||||
|
github.com/ProtonMail/gluon v0.17.1-0.20240918150504-3b2e7f40d961 h1:kCaz78X7OKETvK6AGHeyggHKxDBcqX7EWHf7spJ+D3g=
|
||||||
|
github.com/ProtonMail/gluon v0.17.1-0.20240918150504-3b2e7f40d961/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||||
|
github.com/ProtonMail/gluon v0.17.1-0.20240923094038-e319bf6047c5 h1:LzaUpUj6M2PEBArFCkaimViNpGXDgwHVrdhvYwHLoJQ=
|
||||||
|
github.com/ProtonMail/gluon v0.17.1-0.20240923094038-e319bf6047c5/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||||
|
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602 h1:EoMjWlC32tg46L/07hWoiZfLkqJyxVMcsq4Cyn+Ofqc=
|
||||||
|
github.com/ProtonMail/gluon v0.17.1-0.20240923151549-d23b4bec3602/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||||
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=
|
||||||
@ -36,6 +42,14 @@ github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233 h1:bdoKdh0f66
|
|||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
github.com/ProtonMail/go-crypto v0.0.0-20230717121622-edf196117233/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
|
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/DyZ/qGfMT9htAT7HxqIEbZHsatsx+m8AoV6fc=
|
||||||
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||||
|
github.com/ProtonMail/go-message v0.13.1-0.20240906141354-38c596f2f5a8 h1:+eE7FGX+4Hu8RZaRmSebrDVXyLuowKSaO7ZhQ6ca4+E=
|
||||||
|
github.com/ProtonMail/go-message v0.13.1-0.20240906141354-38c596f2f5a8/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||||
|
github.com/ProtonMail/go-message v0.13.1-0.20240906144417-4083506a9542 h1:5DqSycYnKfUdHiu0yOdiYW5R2hVxoE0Mk4PLSYwqGyg=
|
||||||
|
github.com/ProtonMail/go-message v0.13.1-0.20240906144417-4083506a9542/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||||
|
github.com/ProtonMail/go-message v0.13.1-0.20240910093530-2ada52e7dffb h1:uOKp93u6JFYlBoJJvOhzmHZURcvWmXiqhihGWtT3HtY=
|
||||||
|
github.com/ProtonMail/go-message v0.13.1-0.20240910093530-2ada52e7dffb/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||||
|
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423 h1:p8nBDxvRnvDOyrcePKkPpErWGhDoTqpX8a1c54CcSu0=
|
||||||
|
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
|
||||||
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240612082117-0f92424eed80 h1:cP4+6RFn9vVgYnoDwxBU4EtIAZA+eM4rzOaSZNqZ1xg=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240612082117-0f92424eed80 h1:cP4+6RFn9vVgYnoDwxBU4EtIAZA+eM4rzOaSZNqZ1xg=
|
||||||
@ -56,6 +70,10 @@ github.com/ProtonMail/go-proton-api v0.4.1-0.20240827132526-849231fc34a1 h1:gATl
|
|||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240827132526-849231fc34a1/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240827132526-849231fc34a1/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240829112804-d663a2ef90c2 h1:yx0iejqB5c21HIN5jn9IsbyzUns0dPUUaGfyUHF3TmQ=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240829112804-d663a2ef90c2 h1:yx0iejqB5c21HIN5jn9IsbyzUns0dPUUaGfyUHF3TmQ=
|
||||||
github.com/ProtonMail/go-proton-api v0.4.1-0.20240829112804-d663a2ef90c2/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240829112804-d663a2ef90c2/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||||
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240916123336-3ac75d8041dc h1:SWVPwO1M2jCI1bJHBji/JVU01FpWP/6nzh8NBIjo+Fg=
|
||||||
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240916123336-3ac75d8041dc/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||||
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47 h1:a+3dOyIxJEslN5HxyICM8flY9lnCyJupXNcv6fUaivA=
|
||||||
|
github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47/go.mod h1:3A0cpdo0BIenIPjTG6u8EbzJ8uuJy7rVvM/NaynjCKA=
|
||||||
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
|
||||||
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||||
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI=
|
||||||
|
|||||||
@ -267,6 +267,8 @@ func newBridge(
|
|||||||
|
|
||||||
unleashService := unleash.NewBridgeService(ctx, api, locator, panicHandler)
|
unleashService := unleash.NewBridgeService(ctx, api, locator, panicHandler)
|
||||||
|
|
||||||
|
observabilityService := observability.NewService(ctx, panicHandler)
|
||||||
|
|
||||||
bridge := &Bridge{
|
bridge := &Bridge{
|
||||||
vault: vault,
|
vault: vault,
|
||||||
|
|
||||||
@ -306,11 +308,11 @@ func newBridge(
|
|||||||
lastVersion: lastVersion,
|
lastVersion: lastVersion,
|
||||||
|
|
||||||
tasks: tasks,
|
tasks: tasks,
|
||||||
syncService: syncservice.NewService(reporter, panicHandler),
|
syncService: syncservice.NewService(panicHandler, observabilityService),
|
||||||
|
|
||||||
unleashService: unleashService,
|
unleashService: unleashService,
|
||||||
|
|
||||||
observabilityService: observability.NewService(ctx, panicHandler),
|
observabilityService: observabilityService,
|
||||||
|
|
||||||
notificationStore: notifications.NewStore(locator.ProvideNotificationsCachePath),
|
notificationStore: notifications.NewStore(locator.ProvideNotificationsCachePath),
|
||||||
}
|
}
|
||||||
@ -342,7 +344,7 @@ func newBridge(
|
|||||||
|
|
||||||
bridge.unleashService.Run()
|
bridge.unleashService.Run()
|
||||||
|
|
||||||
bridge.observabilityService.Run()
|
bridge.observabilityService.Run(bridge)
|
||||||
|
|
||||||
return bridge, nil
|
return bridge, nil
|
||||||
}
|
}
|
||||||
@ -710,5 +712,13 @@ func (bridge *Bridge) GetFeatureFlagValue(key string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) PushObservabilityMetric(metric proton.ObservabilityMetric) {
|
func (bridge *Bridge) PushObservabilityMetric(metric proton.ObservabilityMetric) {
|
||||||
bridge.observabilityService.AddMetric(metric)
|
bridge.observabilityService.AddMetrics(metric)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) {
|
||||||
|
bridge.observabilityService.AddDistinctMetrics(errType, metrics...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bridge *Bridge) ModifyObservabilityHeartbeatInterval(duration time.Duration) {
|
||||||
|
bridge.observabilityService.ModifyHeartbeatInterval(duration)
|
||||||
}
|
}
|
||||||
|
|||||||
49
internal/bridge/mocks/observability_mocks.go
Normal file
49
internal/bridge/mocks/observability_mocks.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
reflect "reflect"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockObservabilitySender struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockObservabilitySenderRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockObservabilitySenderRecorder struct {
|
||||||
|
mock *MockObservabilitySender
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockObservabilitySender(ctrl *gomock.Controller) *MockObservabilitySender {
|
||||||
|
mock := &MockObservabilitySender{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockObservabilitySenderRecorder{mock: mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockObservabilitySender) EXPECT() *MockObservabilitySenderRecorder { return m.recorder }
|
||||||
|
|
||||||
|
func (m *MockObservabilitySender) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "AddDistinctMetrics", errType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockObservabilitySender) AddMetrics(metrics ...proton.ObservabilityMetric) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "AddMetrics", metrics)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mr *MockObservabilitySenderRecorder) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock,
|
||||||
|
"AddDistinctMetrics",
|
||||||
|
reflect.TypeOf((*MockObservabilitySender)(nil).AddDistinctMetrics),
|
||||||
|
errType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mr *MockObservabilitySenderRecorder) AddMetrics(metrics ...proton.ObservabilityMetric) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMetrics", reflect.TypeOf((*MockObservabilitySender)(nil).AddMetrics), metrics)
|
||||||
|
}
|
||||||
@ -95,3 +95,70 @@ func TestBridge_Observability(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBridge_Observability_Heartbeat(t *testing.T) {
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
throttlePeriod := time.Millisecond * 300
|
||||||
|
observability.ModifyThrottlePeriod(throttlePeriod)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||||
|
bridge.ModifyObservabilityHeartbeatInterval(throttlePeriod)
|
||||||
|
|
||||||
|
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
time.Sleep(time.Millisecond * 150)
|
||||||
|
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
time.Sleep(time.Millisecond * 200)
|
||||||
|
require.Equal(t, 1, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
time.Sleep(time.Millisecond * 350)
|
||||||
|
require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
time.Sleep(time.Millisecond * 350)
|
||||||
|
require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBridge_Observability_UserMetric(t *testing.T) {
|
||||||
|
testMetric := proton.ObservabilityMetric{
|
||||||
|
Name: "test1",
|
||||||
|
Version: 1,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
Data: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, vaultKey []byte) {
|
||||||
|
userMetricPeriod := time.Millisecond * 200
|
||||||
|
heartbeatPeriod := time.Second * 10
|
||||||
|
throttlePeriod := time.Millisecond * 100
|
||||||
|
observability.ModifyUserMetricInterval(userMetricPeriod)
|
||||||
|
observability.ModifyThrottlePeriod(throttlePeriod)
|
||||||
|
|
||||||
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, vaultKey, func(bridge *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
|
require.NoError(t, getErr(bridge.LoginFull(ctx, username, password, nil, nil)))
|
||||||
|
bridge.ModifyObservabilityHeartbeatInterval(heartbeatPeriod)
|
||||||
|
|
||||||
|
time.Sleep(throttlePeriod)
|
||||||
|
require.Equal(t, 0, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
|
||||||
|
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
||||||
|
time.Sleep(throttlePeriod)
|
||||||
|
// We're expecting two observability metrics to be sent, the actual metric + the user metric.
|
||||||
|
require.Equal(t, 2, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
|
||||||
|
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
||||||
|
time.Sleep(throttlePeriod)
|
||||||
|
// We're expecting only a single metric to be sent, since the user metric update has been sent already within the predefined period.
|
||||||
|
require.Equal(t, 3, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
|
||||||
|
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
||||||
|
time.Sleep(throttlePeriod)
|
||||||
|
// Two metric updates should be sent again.
|
||||||
|
require.Equal(t, 5, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
|
||||||
|
bridge.PushDistinctObservabilityMetrics(observability.SyncError, testMetric)
|
||||||
|
time.Sleep(throttlePeriod)
|
||||||
|
// Only a single one should be sent.
|
||||||
|
require.Equal(t, 6, len(s.GetObservabilityStatistics().Metrics))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -29,13 +29,12 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/golang/mock/gomock"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBridge_Report(t *testing.T) {
|
func TestBridge_Report(t *testing.T) {
|
||||||
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
|
||||||
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
|
||||||
defer done()
|
defer done()
|
||||||
|
|
||||||
@ -56,12 +55,6 @@ func TestBridge_Report(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer func() { require.NoError(t, conn.Close()) }()
|
defer func() { require.NoError(t, conn.Close()) }()
|
||||||
|
|
||||||
// Sending garbage to the IMAP port should cause the bridge to report it.
|
|
||||||
mocks.Reporter.EXPECT().ReportMessageWithContext(
|
|
||||||
gomock.Eq("Failed to parse IMAP command"),
|
|
||||||
gomock.Any(),
|
|
||||||
).Return(nil)
|
|
||||||
|
|
||||||
// Read lines from the IMAP port.
|
// Read lines from the IMAP port.
|
||||||
lineCh := liner.New(conn).Lines(func() error { return nil })
|
lineCh := liner.New(conn).Lines(func() error { return nil })
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
@ -115,6 +116,17 @@ func (bridge *Bridge) installUpdate(ctx context.Context, job installJob) {
|
|||||||
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.version)
|
err := bridge.updater.InstallUpdate(ctx, bridge.api, job.version)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
case errors.Is(err, updater.ErrDownloadVerify):
|
||||||
|
// BRIDGE-207: if download or verification fails, we do not want to trigger a manual update. We report in the log and to Sentry
|
||||||
|
// and we fail silently.
|
||||||
|
log.WithError(err).Error("The update could not be installed, but we will fail silently")
|
||||||
|
if reporterErr := bridge.reporter.ReportMessageWithContext(
|
||||||
|
"Cannot download or verify update",
|
||||||
|
reporter.Context{"error": err},
|
||||||
|
); reporterErr != nil {
|
||||||
|
log.WithError(reporterErr).Error("Failed to report update error")
|
||||||
|
}
|
||||||
|
|
||||||
case errors.Is(err, updater.ErrUpdateAlreadyInstalled):
|
case errors.Is(err, updater.ErrUpdateAlreadyInstalled):
|
||||||
log.Info("The update was already installed")
|
log.Info("The update was already installed")
|
||||||
|
|
||||||
|
|||||||
@ -571,7 +571,6 @@ func (bridge *Bridge) addUserWithVault(
|
|||||||
isNew,
|
isNew,
|
||||||
bridge.notificationStore,
|
bridge.notificationStore,
|
||||||
bridge.unleashService.GetFlagValue,
|
bridge.unleashService.GetFlagValue,
|
||||||
bridge.observabilityService.AddMetric,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create user: %w", err)
|
return fmt.Errorf("failed to create user: %w", err)
|
||||||
|
|||||||
@ -38,9 +38,6 @@ func (bridge *Bridge) handleUserEvent(ctx context.Context, user *user.User, even
|
|||||||
|
|
||||||
case events.UserLoadedCheckResync:
|
case events.UserLoadedCheckResync:
|
||||||
user.VerifyResyncAndExecute()
|
user.VerifyResyncAndExecute()
|
||||||
|
|
||||||
case events.UncategorizedEventError:
|
|
||||||
bridge.handleUncategorizedErrorEvent(event)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,12 +64,3 @@ func (bridge *Bridge) handleUserBadEvent(ctx context.Context, user *user.User, e
|
|||||||
user.OnBadEvent(ctx)
|
user.OnBadEvent(ctx)
|
||||||
}, bridge.usersLock)
|
}, bridge.usersLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bridge *Bridge) handleUncategorizedErrorEvent(event events.UncategorizedEventError) {
|
|
||||||
if rerr := bridge.reporter.ReportMessageWithContext("Failed to handle due to uncategorized error", reporter.Context{
|
|
||||||
"error_type": internal.ErrCauseType(event.Error),
|
|
||||||
"error": event.Error,
|
|
||||||
}); rerr != nil {
|
|
||||||
logrus.WithField("pkg", "bridge/event").WithError(rerr).Error("Failed to report failed event handling")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -54,6 +54,7 @@ QString const bridgeGUILock = "bridge-v3-gui.lock"; ///< The file name used for
|
|||||||
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
QString const exeName = "bridge" + exeSuffix; ///< The bridge executable file name.*
|
||||||
qint64 constexpr grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
|
qint64 constexpr grpcServiceConfigWaitDelayMs = 180000; ///< The wait delay for the gRPC config file in milliseconds.
|
||||||
QString const waitFlag = "--wait"; ///< The wait command-line flag.
|
QString const waitFlag = "--wait"; ///< The wait command-line flag.
|
||||||
|
QString const orphanInstanceException = "An orphan instance of bridge is already running. Please terminate it and relaunch the application.";
|
||||||
|
|
||||||
} // anonymous namespace
|
} // anonymous namespace
|
||||||
|
|
||||||
@ -317,7 +318,7 @@ int main(int argc, char *argv[]) {
|
|||||||
QString bridgeExe;
|
QString bridgeExe;
|
||||||
if (!cliOptions.attach) {
|
if (!cliOptions.attach) {
|
||||||
if (isBridgeRunning()) {
|
if (isBridgeRunning()) {
|
||||||
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
|
throw Exception(orphanInstanceException,
|
||||||
QString(), __FUNCTION__, tailOfLatestBridgeLog(sessionID));
|
QString(), __FUNCTION__, tailOfLatestBridgeLog(sessionID));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -413,13 +414,18 @@ int main(int argc, char *argv[]) {
|
|||||||
lock.unlock();
|
lock.unlock();
|
||||||
return result;
|
return result;
|
||||||
} catch (Exception const &e) {
|
} catch (Exception const &e) {
|
||||||
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
|
|
||||||
QString message = e.qwhat();
|
QString message = e.qwhat();
|
||||||
if (e.showSupportLink()) {
|
if (e.showSupportLink()) {
|
||||||
message += R"(<br/><br/>If the issue persists, please contact our <a href="https://proton.me/support/contact">customer support</a>.)";
|
message += R"(<br/><br/>If the issue persists, please contact our <a href="https://proton.me/support/contact">customer support</a>.)";
|
||||||
}
|
}
|
||||||
QMessageBox::critical(nullptr, "Error", message);
|
QMessageBox::critical(nullptr, "Error", message);
|
||||||
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :" << e.detailedWhat() << "\n";
|
|
||||||
|
if (e.qwhat() != orphanInstanceException) {
|
||||||
|
sentry_uuid_s const uuid = reportSentryException("Exception occurred during main", e);
|
||||||
|
QTextStream(stderr) << "reportID: " << QByteArray(uuid.bytes, 16).toHex() << " Captured exception :"
|
||||||
|
<< e.detailedWhat() << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,11 @@ var (
|
|||||||
// getInitials based on webapp implementation:
|
// getInitials based on webapp implementation:
|
||||||
// https://github.com/ProtonMail/WebClients/blob/55d96a8b4afaaa4372fc5f1ef34953f2070fd7ec/packages/shared/lib/helpers/string.ts#L145
|
// https://github.com/ProtonMail/WebClients/blob/55d96a8b4afaaa4372fc5f1ef34953f2070fd7ec/packages/shared/lib/helpers/string.ts#L145
|
||||||
func getInitials(fullName string) string {
|
func getInitials(fullName string) string {
|
||||||
|
fullName = strings.TrimSpace(fullName)
|
||||||
|
if fullName == "" {
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
|
||||||
words := strings.Split(
|
words := strings.Split(
|
||||||
reMultiSpaces.ReplaceAllString(fullName, " "),
|
reMultiSpaces.ReplaceAllString(fullName, " "),
|
||||||
" ",
|
" ",
|
||||||
|
|||||||
45
internal/frontend/grpc/utils_test.go
Normal file
45
internal/frontend/grpc/utils_test.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_GetInitials(t *testing.T) {
|
||||||
|
require.Equal(t, "?", getInitials(""))
|
||||||
|
require.Equal(t, "T", getInitials(" test"))
|
||||||
|
require.Equal(t, "T", getInitials("test "))
|
||||||
|
require.Equal(t, "T", getInitials(" test "))
|
||||||
|
require.Equal(t, "JD", getInitials(" John Doe "))
|
||||||
|
require.Equal(t, "J", getInitials(" JohnDoe@proton.me "))
|
||||||
|
require.Equal(t, "JD", getInitials("\t\r\n John Doe \t\r\n "))
|
||||||
|
|
||||||
|
require.Equal(t, "T", getInitials("TestTestman"))
|
||||||
|
require.Equal(t, "TT", getInitials("Test Testman"))
|
||||||
|
require.Equal(t, "J", getInitials("JamesJoyce"))
|
||||||
|
require.Equal(t, "J", getInitials("JamesJoyceJeremy"))
|
||||||
|
require.Equal(t, "J", getInitials("james.joyce"))
|
||||||
|
require.Equal(t, "JJ", getInitials("James Joyce"))
|
||||||
|
require.Equal(t, "JM", getInitials("James Joyce Mahabharata"))
|
||||||
|
require.Equal(t, "JL", getInitials("James Joyce Jeremy Lin"))
|
||||||
|
require.Equal(t, "JM", getInitials("Jean Michel"))
|
||||||
|
require.Equal(t, "GC", getInitials("George Michael Carrie"))
|
||||||
|
}
|
||||||
@ -31,7 +31,6 @@ import (
|
|||||||
"github.com/ProtonMail/gluon/connector"
|
"github.com/ProtonMail/gluon/connector"
|
||||||
"github.com/ProtonMail/gluon/imap"
|
"github.com/ProtonMail/gluon/imap"
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/gluon/rfc5322"
|
|
||||||
"github.com/ProtonMail/gluon/rfc822"
|
"github.com/ProtonMail/gluon/rfc822"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
@ -690,8 +689,6 @@ func (s *Connector) importMessage(
|
|||||||
|
|
||||||
isDraft := slices.Contains(labelIDs, proton.DraftsLabel)
|
isDraft := slices.Contains(labelIDs, proton.DraftsLabel)
|
||||||
|
|
||||||
s.reportGODT3185(isDraft, addr.Email, p, s.addressMode == usertypes.AddressModeCombined)
|
|
||||||
|
|
||||||
if err := s.identityState.WithAddrKR(s.addrID, func(_, addrKR *crypto.KeyRing) error {
|
if err := s.identityState.WithAddrKR(s.addrID, func(_, addrKR *crypto.KeyRing) error {
|
||||||
primaryKey, errKey := addrKR.FirstKey()
|
primaryKey, errKey := addrKR.FirstKey()
|
||||||
if errKey != nil {
|
if errKey != nil {
|
||||||
@ -877,80 +874,3 @@ func stripPlusAlias(a string) string {
|
|||||||
func equalAddresses(a, b string) bool {
|
func equalAddresses(a, b string) bool {
|
||||||
return strings.EqualFold(stripPlusAlias(a), stripPlusAlias(b))
|
return strings.EqualFold(stripPlusAlias(a), stripPlusAlias(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Connector) reportGODT3185(isDraft bool, defaultAddr string, p *parser.Parser, isCombinedMode bool) {
|
|
||||||
reportAction := "draft"
|
|
||||||
if !isDraft {
|
|
||||||
reportAction = "import"
|
|
||||||
}
|
|
||||||
|
|
||||||
reportMode := "combined"
|
|
||||||
if !isCombinedMode {
|
|
||||||
reportMode = "split"
|
|
||||||
}
|
|
||||||
|
|
||||||
senderAddr := ""
|
|
||||||
if p != nil && p.Root() != nil && p.Root().Header.Len() != 0 {
|
|
||||||
addrField := p.Root().Header.Get("From")
|
|
||||||
if addrField == "" {
|
|
||||||
addrField = p.Root().Header.Get("Sender")
|
|
||||||
}
|
|
||||||
if addrField != "" {
|
|
||||||
sender, err := rfc5322.ParseAddressList(addrField)
|
|
||||||
if err == nil && len(sender) > 0 {
|
|
||||||
senderAddr = sender[0].Address
|
|
||||||
} else {
|
|
||||||
s.log.WithError(err).Warn("Invalid sender address in reporter")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if equalAddresses(defaultAddr, senderAddr) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isDisabled := false
|
|
||||||
isUserAddress := false
|
|
||||||
for _, a := range s.identityState.GetAddresses() {
|
|
||||||
if !equalAddresses(a.Email, senderAddr) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
isUserAddress = true
|
|
||||||
isDisabled = !bool(a.Send) || (a.Status != proton.AddressStatusEnabled)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isUserAddress && senderAddr != "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reportResult := "using sender address"
|
|
||||||
|
|
||||||
if !isCombinedMode {
|
|
||||||
reportResult = "error address not match"
|
|
||||||
}
|
|
||||||
|
|
||||||
reportAddress := ""
|
|
||||||
if senderAddr == "" {
|
|
||||||
reportAddress = " invalid"
|
|
||||||
reportResult = "error import/draft"
|
|
||||||
}
|
|
||||||
|
|
||||||
if isDisabled {
|
|
||||||
reportAddress = " disabled"
|
|
||||||
if isDraft {
|
|
||||||
reportResult = "error draft"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
report := fmt.Sprintf(
|
|
||||||
"GODT-3185: %s with non-default%s address in %s mode: %s",
|
|
||||||
reportAction, reportAddress, reportMode, reportResult,
|
|
||||||
)
|
|
||||||
|
|
||||||
s.log.Warn(report)
|
|
||||||
if s.reporter != nil {
|
|
||||||
_ = s.reporter.ReportMessage(report)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -0,0 +1,67 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package evtloopmsgevents
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
messageEventErrorCaseSchemaName = "bridge_event_loop_message_event_failures_total"
|
||||||
|
messageEventErrorCaseSchemaVersion = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateMessageEventFailureObservabilityMetric(eventType string) proton.ObservabilityMetric {
|
||||||
|
return proton.ObservabilityMetric{
|
||||||
|
Name: messageEventErrorCaseSchemaName,
|
||||||
|
Version: messageEventErrorCaseSchemaVersion,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"Value": 1,
|
||||||
|
"Labels": map[string]string{
|
||||||
|
"eventType": eventType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateMessageEventFailureCreateMessageMetric() proton.ObservabilityMetric {
|
||||||
|
return generateMessageEventFailureObservabilityMetric("createMessageEvent")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateMessageEventFailureDeleteMessageMetric() proton.ObservabilityMetric {
|
||||||
|
return generateMessageEventFailureObservabilityMetric("deleteMessageEvent")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateMessageEventFailureUpdateMetric() proton.ObservabilityMetric {
|
||||||
|
return generateMessageEventFailureObservabilityMetric("updateEvent")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateMessageEventFailedToBuildMessage() proton.ObservabilityMetric {
|
||||||
|
return generateMessageEventFailureObservabilityMetric("failedToBuildMessage")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateMessageEventFailedToBuildDraft() proton.ObservabilityMetric {
|
||||||
|
return generateMessageEventFailureObservabilityMetric("failedToBuildDraft")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateMessageEventUpdateChannelDoesNotExist() proton.ObservabilityMetric {
|
||||||
|
return generateMessageEventFailureObservabilityMetric("messageUpdateChannelDoesNotExist")
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package syncmsgevents
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
syncEventErrorCaseSchemaName = "bridge_sync_message_event_failures_total"
|
||||||
|
syncEventErrorCaseSchemaVersion = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateSyncEventFailureObservabilityMetric(eventType string) proton.ObservabilityMetric {
|
||||||
|
return proton.ObservabilityMetric{
|
||||||
|
Name: syncEventErrorCaseSchemaName,
|
||||||
|
Version: syncEventErrorCaseSchemaVersion,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"Value": 1,
|
||||||
|
"Labels": map[string]string{
|
||||||
|
"eventType": eventType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateSyncFailureCreateMessageEventMetric() proton.ObservabilityMetric {
|
||||||
|
return generateSyncEventFailureObservabilityMetric("createMessageEvent")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateSyncFailureDeleteMessageEventMetric() proton.ObservabilityMetric {
|
||||||
|
return generateSyncEventFailureObservabilityMetric("deleteMessageEvent")
|
||||||
|
}
|
||||||
@ -30,6 +30,7 @@ import (
|
|||||||
"github.com/ProtonMail/gluon/watcher"
|
"github.com/ProtonMail/gluon/watcher"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
"github.com/ProtonMail/proton-bridge/v3/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/orderedtasks"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/orderedtasks"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/sendrecorder"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/sendrecorder"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice"
|
||||||
@ -96,6 +97,8 @@ type Service struct {
|
|||||||
syncConfigPath string
|
syncConfigPath string
|
||||||
lastHandledEventID string
|
lastHandledEventID string
|
||||||
isSyncing atomic.Bool
|
isSyncing atomic.Bool
|
||||||
|
|
||||||
|
observabilitySender observability.Sender
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
@ -116,6 +119,7 @@ func NewService(
|
|||||||
syncConfigDir string,
|
syncConfigDir string,
|
||||||
maxSyncMemory uint64,
|
maxSyncMemory uint64,
|
||||||
showAllMail bool,
|
showAllMail bool,
|
||||||
|
observabilitySender observability.Sender,
|
||||||
) *Service {
|
) *Service {
|
||||||
subscriberName := fmt.Sprintf("imap-%v", identityState.User.ID)
|
subscriberName := fmt.Sprintf("imap-%v", identityState.User.ID)
|
||||||
|
|
||||||
@ -160,6 +164,8 @@ func NewService(
|
|||||||
syncMessageBuilder: syncMessageBuilder,
|
syncMessageBuilder: syncMessageBuilder,
|
||||||
syncReporter: syncReporter,
|
syncReporter: syncReporter,
|
||||||
syncConfigPath: GetSyncConfigPath(syncConfigDir, identityState.User.ID),
|
syncConfigPath: GetSyncConfigPath(syncConfigDir, identityState.User.ID),
|
||||||
|
|
||||||
|
observabilitySender: observabilitySender,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,11 +26,11 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/gluon"
|
"github.com/ProtonMail/gluon"
|
||||||
"github.com/ProtonMail/gluon/imap"
|
"github.com/ProtonMail/gluon/imap"
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal"
|
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||||
|
obsMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/evtloopmsgevents"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
@ -46,7 +46,7 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
|
|||||||
case proton.EventCreate:
|
case proton.EventCreate:
|
||||||
updates, err := onMessageCreated(logging.WithLogrusField(ctx, "action", "create message"), s, event.Message, false)
|
updates, err := onMessageCreated(logging.WithLogrusField(ctx, "action", "create message"), s, event.Message, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reportError(s.reporter, s.log, "Failed to apply create message event", err)
|
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailureCreateMessageMetric())
|
||||||
return fmt.Errorf("failed to handle create message event: %w", err)
|
return fmt.Errorf("failed to handle create message event: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
|
|||||||
event,
|
event,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reportError(s.reporter, s.log, "Failed to apply update draft message event", err)
|
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailureUpdateMetric())
|
||||||
return fmt.Errorf("failed to handle update draft event: %w", err)
|
return fmt.Errorf("failed to handle update draft event: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
|
|||||||
event.Message,
|
event.Message,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reportError(s.reporter, s.log, "Failed to apply update message event", err)
|
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailureUpdateMetric())
|
||||||
return fmt.Errorf("failed to handle update message event: %w", err)
|
return fmt.Errorf("failed to handle update message event: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,6 +113,7 @@ func (s *Service) HandleMessageEvents(ctx context.Context, events []proton.Messa
|
|||||||
)
|
)
|
||||||
|
|
||||||
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
|
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
|
||||||
|
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailureDeleteMessageMetric())
|
||||||
return fmt.Errorf("failed to handle delete message event in gluon: %w", err)
|
return fmt.Errorf("failed to handle delete message event in gluon: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -158,8 +159,7 @@ func onMessageCreated(
|
|||||||
s.log.WithError(err).Error("Failed to add failed message ID to vault")
|
s.log.WithError(err).Error("Failed to add failed message ID to vault")
|
||||||
}
|
}
|
||||||
|
|
||||||
reportErrorAndMessageID(s.reporter, s.log, "Failed to build message (event create)", res.err, res.messageID)
|
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailedToBuildMessage())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,8 +221,7 @@ func onMessageUpdateDraftOrSent(ctx context.Context, s *Service, event proton.Me
|
|||||||
s.log.WithError(err).Error("Failed to add failed message ID to vault")
|
s.log.WithError(err).Error("Failed to add failed message ID to vault")
|
||||||
}
|
}
|
||||||
|
|
||||||
reportErrorAndMessageID(s.reporter, s.log, "Failed to build draft message (event update)", res.err, res.messageID)
|
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventFailedToBuildDraft())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,24 +298,6 @@ func onMessageDeleted(ctx context.Context, s *Service, event proton.MessageEvent
|
|||||||
return updates
|
return updates
|
||||||
}
|
}
|
||||||
|
|
||||||
func reportError(r reporter.Reporter, entry *logrus.Entry, title string, err error) {
|
|
||||||
reportErrorNoContextCancel(r, entry, title, err, reporter.Context{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func reportErrorAndMessageID(r reporter.Reporter, entry *logrus.Entry, title string, err error, messgeID string) {
|
|
||||||
reportErrorNoContextCancel(r, entry, title, err, reporter.Context{"messageID": messgeID})
|
|
||||||
}
|
|
||||||
|
|
||||||
func reportErrorNoContextCancel(r reporter.Reporter, entry *logrus.Entry, title string, err error, reportContext reporter.Context) {
|
|
||||||
if !errors.Is(err, context.Canceled) {
|
|
||||||
reportContext["error"] = err
|
|
||||||
reportContext["error_type"] = internal.ErrCauseType(err)
|
|
||||||
if rerr := r.ReportMessageWithContext(title, reportContext); rerr != nil {
|
|
||||||
entry.WithError(err).WithField("title", title).Error("Failed to report message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// safePublishMessageUpdate handles the rare case where the address' update channel may have been deleted in the same
|
// safePublishMessageUpdate handles the rare case where the address' update channel may have been deleted in the same
|
||||||
// event. This rare case can take place if in the same event fetch request there is an update for delete address and
|
// event. This rare case can take place if in the same event fetch request there is an update for delete address and
|
||||||
// create/update message.
|
// create/update message.
|
||||||
@ -341,7 +322,7 @@ func safePublishMessageUpdate(ctx context.Context, s *Service, addressID string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
logrus.Warnf("Update channel not found for address %v, it may have been already deleted", addressID)
|
logrus.Warnf("Update channel not found for address %v, it may have been already deleted", addressID)
|
||||||
_ = s.reporter.ReportMessage("Message Update channel does not exist")
|
s.observabilitySender.AddDistinctMetrics(observability.EventLoopError, obsMetrics.GenerateMessageEventUpdateChannelDoesNotExist())
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,8 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||||
|
obsMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/syncmsgevents"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -55,7 +57,7 @@ func (s syncMessageEventHandler) HandleMessageEvents(ctx context.Context, events
|
|||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reportError(s.service.reporter, s.service.log, "Failed to apply create message event", err)
|
s.service.observabilitySender.AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateSyncFailureCreateMessageEventMetric())
|
||||||
return fmt.Errorf("failed to handle create message event: %w", err)
|
return fmt.Errorf("failed to handle create message event: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,6 +73,7 @@ func (s syncMessageEventHandler) HandleMessageEvents(ctx context.Context, events
|
|||||||
)
|
)
|
||||||
|
|
||||||
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
|
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
|
||||||
|
s.service.observabilitySender.AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateSyncFailureDeleteMessageEventMetric())
|
||||||
return fmt.Errorf("failed to handle delete message event in gluon: %w", err)
|
return fmt.Errorf("failed to handle delete message event in gluon: %w", err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -45,14 +45,15 @@ type Service struct {
|
|||||||
store *Store
|
store *Store
|
||||||
|
|
||||||
getFlagValueFn unleash.GetFlagValueFn
|
getFlagValueFn unleash.GetFlagValueFn
|
||||||
pushObservabilityMetricFn observability.PushObsMetricFn
|
|
||||||
|
observabilitySender observability.Sender
|
||||||
}
|
}
|
||||||
|
|
||||||
const bitfieldRegexPattern = `^\\\d+`
|
const bitfieldRegexPattern = `^\\\d+`
|
||||||
const disableNotificationsKillSwitch = "InboxBridgeEventLoopNotificationDisabled"
|
const disableNotificationsKillSwitch = "InboxBridgeEventLoopNotificationDisabled"
|
||||||
|
|
||||||
func NewService(userID string, service userevents.Subscribable, eventPublisher events.EventPublisher, store *Store,
|
func NewService(userID string, service userevents.Subscribable, eventPublisher events.EventPublisher, store *Store,
|
||||||
getFlagFn unleash.GetFlagValueFn, pushMetricFn observability.PushObsMetricFn) *Service {
|
getFlagFn unleash.GetFlagValueFn, observabilitySender observability.Sender) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
userID: userID,
|
userID: userID,
|
||||||
|
|
||||||
@ -69,7 +70,7 @@ func NewService(userID string, service userevents.Subscribable, eventPublisher e
|
|||||||
store: store,
|
store: store,
|
||||||
|
|
||||||
getFlagValueFn: getFlagFn,
|
getFlagValueFn: getFlagFn,
|
||||||
pushObservabilityMetricFn: pushMetricFn,
|
observabilitySender: observabilitySender,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,7 +111,7 @@ func (s *Service) HandleNotificationEvents(ctx context.Context, notificationEven
|
|||||||
s.log.Debug("Handling notification events")
|
s.log.Debug("Handling notification events")
|
||||||
|
|
||||||
// Publish observability metrics that we've received notifications
|
// Publish observability metrics that we've received notifications
|
||||||
s.pushObservabilityMetricFn(GenerateReceivedMetric(len(notificationEvents)))
|
s.observabilitySender.AddMetrics(GenerateReceivedMetric(len(notificationEvents)))
|
||||||
|
|
||||||
for _, event := range notificationEvents {
|
for _, event := range notificationEvents {
|
||||||
ctx = logging.WithLogrusField(ctx, "notificationID", event.ID)
|
ctx = logging.WithLogrusField(ctx, "notificationID", event.ID)
|
||||||
@ -133,7 +134,7 @@ func (s *Service) HandleNotificationEvents(ctx context.Context, notificationEven
|
|||||||
Subtitle: event.Payload.Subtitle, Body: event.Payload.Body})
|
Subtitle: event.Payload.Subtitle, Body: event.Payload.Body})
|
||||||
|
|
||||||
// Publish observability metric that we've successfully processed notifications
|
// Publish observability metric that we've successfully processed notifications
|
||||||
s.pushObservabilityMetricFn(GenerateProcessedMetric(1))
|
s.observabilitySender.AddMetrics(GenerateProcessedMetric(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -109,7 +109,7 @@ func (s *Store) readCache() {
|
|||||||
|
|
||||||
file, err := os.Open(s.cacheFilepath)
|
file, err := os.Open(s.cacheFilepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.WithError(err).Error("Unable to open cache file")
|
s.log.WithError(err).Info("Unable to open cache file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
47
internal/services/observability/distinction_error_types.go
Normal file
47
internal/services/observability/distinction_error_types.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package observability
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// DistinctionErrorTypeEnum - maps to the specific error schema for which we
|
||||||
|
// want to send a user update.
|
||||||
|
type DistinctionErrorTypeEnum int
|
||||||
|
|
||||||
|
const (
|
||||||
|
SyncError DistinctionErrorTypeEnum = iota
|
||||||
|
EventLoopError
|
||||||
|
)
|
||||||
|
|
||||||
|
// errorSchemaMap - maps between the DistinctionErrorTypeEnum and the relevant schema name.
|
||||||
|
var errorSchemaMap = map[DistinctionErrorTypeEnum]string{ //nolint:gochecknoglobals
|
||||||
|
SyncError: "bridge_sync_errors_users_total",
|
||||||
|
EventLoopError: "bridge_event_loop_events_errors_users_total",
|
||||||
|
}
|
||||||
|
|
||||||
|
// createLastSentMap - needs to be updated whenever we make changes to the enum.
|
||||||
|
func createLastSentMap() map[DistinctionErrorTypeEnum]time.Time {
|
||||||
|
registerTime := time.Now().Add(-updateInterval)
|
||||||
|
lastSentMap := make(map[DistinctionErrorTypeEnum]time.Time)
|
||||||
|
|
||||||
|
for errType := SyncError; errType <= EventLoopError; errType++ {
|
||||||
|
lastSentMap[errType] = registerTime
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastSentMap
|
||||||
|
}
|
||||||
170
internal/services/observability/distinction_utility.go
Normal file
170
internal/services/observability/distinction_utility.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package observability
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
|
)
|
||||||
|
|
||||||
|
var updateInterval = time.Minute * 5 //nolint:gochecknoglobals
|
||||||
|
|
||||||
|
type observabilitySender interface {
|
||||||
|
addMetricsIfClients(metric ...proton.ObservabilityMetric)
|
||||||
|
}
|
||||||
|
|
||||||
|
// distinctionUtility - used to discern whether X number of events stem from Y number of users.
|
||||||
|
type distinctionUtility struct {
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
panicHandler async.PanicHandler
|
||||||
|
|
||||||
|
lastSentMap map[DistinctionErrorTypeEnum]time.Time // Ensures we don't step over the limit of one user update every 5 mins.
|
||||||
|
|
||||||
|
observabilitySender observabilitySender
|
||||||
|
settingsGetter settingsGetter
|
||||||
|
|
||||||
|
userPlanUnsafe string
|
||||||
|
userPlanLock sync.Mutex
|
||||||
|
|
||||||
|
heartbeatData heartbeatData
|
||||||
|
heartbeatDataLock sync.Mutex
|
||||||
|
heartbeatTicker *time.Ticker
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDistinctionUtility(ctx context.Context, panicHandler async.PanicHandler, observabilitySender observabilitySender) *distinctionUtility {
|
||||||
|
distinctionUtility := &distinctionUtility{
|
||||||
|
ctx: ctx,
|
||||||
|
|
||||||
|
panicHandler: panicHandler,
|
||||||
|
|
||||||
|
lastSentMap: createLastSentMap(),
|
||||||
|
|
||||||
|
observabilitySender: observabilitySender,
|
||||||
|
|
||||||
|
userPlanUnsafe: planUnknown,
|
||||||
|
|
||||||
|
heartbeatData: heartbeatData{},
|
||||||
|
heartbeatTicker: time.NewTicker(updateInterval),
|
||||||
|
}
|
||||||
|
|
||||||
|
return distinctionUtility
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendMetricsWithGuard - schedules the metrics to be sent only if there are authenticated clients.
|
||||||
|
func (d *distinctionUtility) sendMetricsWithGuard(metrics ...proton.ObservabilityMetric) {
|
||||||
|
if d.observabilitySender == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.observabilitySender.addMetricsIfClients(metrics...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *distinctionUtility) setSettingsGetter(getter settingsGetter) {
|
||||||
|
d.settingsGetter = getter
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAndUpdateLastSentMap - checks whether we have sent a relevant user update metric
|
||||||
|
// within the last 5 minutes.
|
||||||
|
func (d *distinctionUtility) checkAndUpdateLastSentMap(key DistinctionErrorTypeEnum) bool {
|
||||||
|
curTime := time.Now()
|
||||||
|
val, ok := d.lastSentMap[key]
|
||||||
|
if !ok {
|
||||||
|
d.lastSentMap[key] = curTime
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if val.Add(updateInterval).Before(curTime) {
|
||||||
|
d.lastSentMap[key] = curTime
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateUserMetric creates the relevant user update metric based on its type
|
||||||
|
// and the relevant settings. In the future this will need to be expanded to support multiple
|
||||||
|
// versions of the metric if we ever decide to change them.
|
||||||
|
func (d *distinctionUtility) generateUserMetric(
|
||||||
|
metricType DistinctionErrorTypeEnum,
|
||||||
|
) proton.ObservabilityMetric {
|
||||||
|
schemaName, ok := errorSchemaMap[metricType]
|
||||||
|
if !ok {
|
||||||
|
return proton.ObservabilityMetric{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateUserMetric(schemaName, d.getUserPlanSafe(),
|
||||||
|
d.getEmailClientUserAgent(),
|
||||||
|
getEnabled(d.getProxyAllowed()),
|
||||||
|
getEnabled(d.getBetaAccessEnabled()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateUserMetric(schemaName, plan, mailClient, dohEnabled, betaAccess string) proton.ObservabilityMetric {
|
||||||
|
return proton.ObservabilityMetric{
|
||||||
|
Name: schemaName,
|
||||||
|
Version: 1,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"Value": 1,
|
||||||
|
"Labels": map[string]string{
|
||||||
|
"plan": plan,
|
||||||
|
"mailClient": mailClient,
|
||||||
|
"dohEnabled": dohEnabled,
|
||||||
|
"betaAccessEnabled": betaAccess,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *distinctionUtility) generateDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) []proton.ObservabilityMetric {
|
||||||
|
d.updateHeartbeatData(errType)
|
||||||
|
|
||||||
|
if d.checkAndUpdateLastSentMap(errType) {
|
||||||
|
metrics = append(metrics, d.generateUserMetric(errType))
|
||||||
|
}
|
||||||
|
return metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *distinctionUtility) getEmailClientUserAgent() string {
|
||||||
|
ua := ""
|
||||||
|
if d.settingsGetter != nil {
|
||||||
|
ua = d.settingsGetter.GetCurrentUserAgent()
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchUserAgent(ua)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *distinctionUtility) getBetaAccessEnabled() bool {
|
||||||
|
if d.settingsGetter == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return d.settingsGetter.GetUpdateChannel() == updater.EarlyChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *distinctionUtility) getProxyAllowed() bool {
|
||||||
|
if d.settingsGetter == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return d.settingsGetter.GetProxyAllowed()
|
||||||
|
}
|
||||||
119
internal/services/observability/heartbeat.go
Normal file
119
internal/services/observability/heartbeat.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package observability
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const genericHeartbeatSchemaName = "bridge_generic_user_heartbeat_total"
|
||||||
|
|
||||||
|
type heartbeatData struct {
|
||||||
|
receivedSyncError bool
|
||||||
|
receivedEventLoopError bool
|
||||||
|
receivedOtherError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *distinctionUtility) resetHeartbeatData() {
|
||||||
|
d.heartbeatData.receivedSyncError = false
|
||||||
|
d.heartbeatData.receivedOtherError = false
|
||||||
|
d.heartbeatData.receivedEventLoopError = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *distinctionUtility) updateHeartbeatData(errType DistinctionErrorTypeEnum) {
|
||||||
|
d.withUpdateHeartbeatDataLock(func() {
|
||||||
|
switch errType {
|
||||||
|
case SyncError:
|
||||||
|
d.heartbeatData.receivedSyncError = true
|
||||||
|
case EventLoopError:
|
||||||
|
d.heartbeatData.receivedEventLoopError = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *distinctionUtility) runHeartbeat() {
|
||||||
|
go func() {
|
||||||
|
defer async.HandlePanic(d.panicHandler)
|
||||||
|
defer d.heartbeatTicker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-d.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-d.heartbeatTicker.C:
|
||||||
|
d.sendHeartbeat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *distinctionUtility) withUpdateHeartbeatDataLock(fn func()) {
|
||||||
|
d.heartbeatDataLock.Lock()
|
||||||
|
defer d.heartbeatDataLock.Unlock()
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendHeartbeat - will only send a heartbeat if there is an authenticated client
|
||||||
|
// otherwise we might end up polluting the cache and therefore our metrics.
|
||||||
|
func (d *distinctionUtility) sendHeartbeat() {
|
||||||
|
d.withUpdateHeartbeatDataLock(func() {
|
||||||
|
d.sendMetricsWithGuard(d.generateHeartbeatUserMetric())
|
||||||
|
d.resetHeartbeatData()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatBool(value bool) string {
|
||||||
|
return fmt.Sprintf("%t", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateHeartbeatUserMetric creates the heartbeat user metric and includes the relevant data.
|
||||||
|
func (d *distinctionUtility) generateHeartbeatUserMetric() proton.ObservabilityMetric {
|
||||||
|
return generateHeartbeatMetric(
|
||||||
|
d.getUserPlanSafe(),
|
||||||
|
d.getEmailClientUserAgent(),
|
||||||
|
getEnabled(d.settingsGetter.GetProxyAllowed()),
|
||||||
|
getEnabled(d.getBetaAccessEnabled()),
|
||||||
|
formatBool(d.heartbeatData.receivedOtherError),
|
||||||
|
formatBool(d.heartbeatData.receivedSyncError),
|
||||||
|
formatBool(d.heartbeatData.receivedEventLoopError),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateHeartbeatMetric(plan, mailClient, dohEnabled, betaAccess, otherError, syncError, eventLoopError string) proton.ObservabilityMetric {
|
||||||
|
return proton.ObservabilityMetric{
|
||||||
|
Name: genericHeartbeatSchemaName,
|
||||||
|
Version: 1,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"Value": 1,
|
||||||
|
"Labels": map[string]string{
|
||||||
|
"plan": plan,
|
||||||
|
"mailClient": mailClient,
|
||||||
|
"dohEnabled": dohEnabled,
|
||||||
|
"betaAccessEnabled": betaAccess,
|
||||||
|
"receivedOtherError": otherError,
|
||||||
|
"receivedSyncError": syncError,
|
||||||
|
"receivedEventLoopError": eventLoopError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
121
internal/services/observability/plan_utils.go
Normal file
121
internal/services/observability/plan_utils.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package observability
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/async"
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
planUnknown = "unknown"
|
||||||
|
planOther = "other"
|
||||||
|
planBusiness = "business"
|
||||||
|
planIndividual = "individual"
|
||||||
|
planGroup = "group"
|
||||||
|
)
|
||||||
|
|
||||||
|
var planHierarchy = map[string]int{ //nolint:gochecknoglobals
|
||||||
|
planBusiness: 4,
|
||||||
|
planGroup: 3,
|
||||||
|
planIndividual: 2,
|
||||||
|
planOther: 1,
|
||||||
|
planUnknown: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
type planGetter interface {
|
||||||
|
GetOrganizationData(ctx context.Context) (proton.OrganizationResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHigherPriority(currentPlan, newPlan string) bool {
|
||||||
|
newRank, ok := planHierarchy[newPlan]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
currentRank, ok2 := planHierarchy[currentPlan]
|
||||||
|
if !ok2 {
|
||||||
|
return true // we don't have a valid plan, might as well replace it
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRank > currentRank
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapUserPlan(planName string) string {
|
||||||
|
if planName == "" {
|
||||||
|
return planUnknown
|
||||||
|
}
|
||||||
|
switch strings.TrimSpace(strings.ToLower(planName)) {
|
||||||
|
case "mail2022":
|
||||||
|
return planIndividual
|
||||||
|
case "bundle2022":
|
||||||
|
return planIndividual
|
||||||
|
case "family2022":
|
||||||
|
return planGroup
|
||||||
|
case "visionary2022":
|
||||||
|
return planGroup
|
||||||
|
case "mailpro2022":
|
||||||
|
return planBusiness
|
||||||
|
case "planbiz2024":
|
||||||
|
return planBusiness
|
||||||
|
case "bundlepro2022":
|
||||||
|
return planBusiness
|
||||||
|
case "bundlepro2024":
|
||||||
|
return planBusiness
|
||||||
|
case "duo2024":
|
||||||
|
return planGroup
|
||||||
|
|
||||||
|
default:
|
||||||
|
return planOther
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *distinctionUtility) setUserPlan(planName string) {
|
||||||
|
if planName == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d.userPlanLock.Lock()
|
||||||
|
defer d.userPlanLock.Unlock()
|
||||||
|
|
||||||
|
userPlanMapped := mapUserPlan(planName)
|
||||||
|
if isHigherPriority(d.userPlanUnsafe, userPlanMapped) {
|
||||||
|
d.userPlanUnsafe = userPlanMapped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *distinctionUtility) registerUserPlan(ctx context.Context, getter planGetter, panicHandler async.PanicHandler) {
|
||||||
|
go func() {
|
||||||
|
defer async.HandlePanic(panicHandler)
|
||||||
|
|
||||||
|
orgRes, err := getter.GetOrganizationData(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d.setUserPlan(orgRes.Organization.PlanName)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *distinctionUtility) getUserPlanSafe() string {
|
||||||
|
d.userPlanLock.Lock()
|
||||||
|
defer d.userPlanLock.Unlock()
|
||||||
|
return d.userPlanUnsafe
|
||||||
|
}
|
||||||
@ -36,13 +36,18 @@ const (
|
|||||||
maxBatchSize = 1000
|
maxBatchSize = 1000
|
||||||
)
|
)
|
||||||
|
|
||||||
type PushObsMetricFn func(metric proton.ObservabilityMetric)
|
|
||||||
|
|
||||||
type client struct {
|
type client struct {
|
||||||
isTelemetryEnabled func(context.Context) bool
|
isTelemetryEnabled func(context.Context) bool
|
||||||
sendMetrics func(context.Context, proton.ObservabilityBatch) error
|
sendMetrics func(context.Context, proton.ObservabilityBatch) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sender - interface maps to the observability service methods,
|
||||||
|
// so we can easily pass them down to relevant components.
|
||||||
|
type Sender interface {
|
||||||
|
AddMetrics(metrics ...proton.ObservabilityMetric)
|
||||||
|
AddDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric)
|
||||||
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@ -62,6 +67,8 @@ type Service struct {
|
|||||||
|
|
||||||
userClientStore map[string]*client
|
userClientStore map[string]*client
|
||||||
userClientStoreLock sync.Mutex
|
userClientStoreLock sync.Mutex
|
||||||
|
|
||||||
|
distinctionUtility *distinctionUtility
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(ctx context.Context, panicHandler async.PanicHandler) *Service {
|
func NewService(ctx context.Context, panicHandler async.PanicHandler) *Service {
|
||||||
@ -85,11 +92,19 @@ func NewService(ctx context.Context, panicHandler async.PanicHandler) *Service {
|
|||||||
userClientStore: make(map[string]*client),
|
userClientStore: make(map[string]*client),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
service.distinctionUtility = newDistinctionUtility(ctx, panicHandler, service)
|
||||||
|
|
||||||
return service
|
return service
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Run() {
|
// Run starts the observability service goroutine.
|
||||||
|
// The function also sets some utility functions to a helper struct aimed at differentiating the amount of users sending metric updates.
|
||||||
|
func (s *Service) Run(settingsGetter settingsGetter) {
|
||||||
s.log.Info("Starting service")
|
s.log.Info("Starting service")
|
||||||
|
|
||||||
|
s.distinctionUtility.setSettingsGetter(settingsGetter)
|
||||||
|
s.distinctionUtility.runHeartbeat()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
s.start()
|
s.start()
|
||||||
}()
|
}()
|
||||||
@ -200,7 +215,7 @@ func (s *Service) scheduleDispatch() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AddMetric(metric proton.ObservabilityMetric) {
|
func (s *Service) addMetrics(metric ...proton.ObservabilityMetric) {
|
||||||
s.withMetricStoreLock(func() {
|
s.withMetricStoreLock(func() {
|
||||||
metricStoreLength := len(s.metricStore)
|
metricStoreLength := len(s.metricStore)
|
||||||
if metricStoreLength >= maxStorageSize {
|
if metricStoreLength >= maxStorageSize {
|
||||||
@ -209,12 +224,32 @@ func (s *Service) AddMetric(metric proton.ObservabilityMetric) {
|
|||||||
dropCount := metricStoreLength - maxStorageSize + 1
|
dropCount := metricStoreLength - maxStorageSize + 1
|
||||||
s.metricStore = s.metricStore[dropCount:]
|
s.metricStore = s.metricStore[dropCount:]
|
||||||
}
|
}
|
||||||
s.metricStore = append(s.metricStore, metric)
|
s.metricStore = append(s.metricStore, metric...)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If the context has been cancelled i.e. the service has been stopped then we should be free to exit.
|
||||||
|
if s.ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
s.sendSignal(s.signalDataArrived)
|
s.sendSignal(s.signalDataArrived)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addMetricsIfClients - will append a metric only if there are authenticated clients
|
||||||
|
// via which we can reach the endpoint.
|
||||||
|
func (s *Service) addMetricsIfClients(metric ...proton.ObservabilityMetric) {
|
||||||
|
hasClients := false
|
||||||
|
s.withUserClientStoreLock(func() {
|
||||||
|
hasClients = len(s.userClientStore) > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
if !hasClients {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.addMetrics(metric...)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) RegisterUserClient(userID string, protonClient *proton.Client, telemetryService *telemetry.Service) {
|
func (s *Service) RegisterUserClient(userID string, protonClient *proton.Client, telemetryService *telemetry.Service) {
|
||||||
s.log.Info("Registering user client, ID:", userID)
|
s.log.Info("Registering user client, ID:", userID)
|
||||||
|
|
||||||
@ -225,6 +260,8 @@ func (s *Service) RegisterUserClient(userID string, protonClient *proton.Client,
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
s.distinctionUtility.registerUserPlan(s.ctx, protonClient, s.panicHandler)
|
||||||
|
|
||||||
// There may be a case where we already have metric updates stored, so try to flush;
|
// There may be a case where we already have metric updates stored, so try to flush;
|
||||||
s.sendSignal(s.signalDataArrived)
|
s.sendSignal(s.signalDataArrived)
|
||||||
}
|
}
|
||||||
@ -279,3 +316,25 @@ func (s *Service) sendSignal(channel chan struct{}) {
|
|||||||
func ModifyThrottlePeriod(duration time.Duration) {
|
func ModifyThrottlePeriod(duration time.Duration) {
|
||||||
throttleDuration = duration
|
throttleDuration = duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) AddMetrics(metrics ...proton.ObservabilityMetric) {
|
||||||
|
s.addMetrics(metrics...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddDistinctMetrics - sends an additional metric related to the user, so we can determine
|
||||||
|
// what number of events come from what number of users.
|
||||||
|
// As the binning interval is what allows us to do this we
|
||||||
|
// should not send these if there are no logged-in users at that moment.
|
||||||
|
func (s *Service) AddDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) {
|
||||||
|
metrics = s.distinctionUtility.generateDistinctMetrics(errType, metrics...)
|
||||||
|
s.addMetricsIfClients(metrics...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModifyHeartbeatInterval - should only be used for testing. Resets the heartbeat ticker.
|
||||||
|
func (s *Service) ModifyHeartbeatInterval(duration time.Duration) {
|
||||||
|
s.distinctionUtility.heartbeatTicker.Reset(duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ModifyUserMetricInterval(duration time.Duration) {
|
||||||
|
updateInterval = duration
|
||||||
|
}
|
||||||
|
|||||||
106
internal/services/observability/test_utils.go
Normal file
106
internal/services/observability/test_utils.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package observability
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateAllUsedDistinctionMetricPermutations() []proton.ObservabilityMetric {
|
||||||
|
planValues := []string{
|
||||||
|
planUnknown,
|
||||||
|
planOther,
|
||||||
|
planBusiness,
|
||||||
|
planIndividual,
|
||||||
|
planGroup}
|
||||||
|
mailClientValues := []string{
|
||||||
|
emailAgentAppleMail,
|
||||||
|
emailAgentOutlook,
|
||||||
|
emailAgentThunderbird,
|
||||||
|
emailAgentOther,
|
||||||
|
emailAgentUnknown,
|
||||||
|
}
|
||||||
|
enabledValues := []string{
|
||||||
|
getEnabled(true), getEnabled(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
var metrics []proton.ObservabilityMetric
|
||||||
|
|
||||||
|
for _, schemaName := range errorSchemaMap {
|
||||||
|
for _, plan := range planValues {
|
||||||
|
for _, mailClient := range mailClientValues {
|
||||||
|
for _, dohEnabled := range enabledValues {
|
||||||
|
for _, betaAccess := range enabledValues {
|
||||||
|
metrics = append(metrics, generateUserMetric(schemaName, plan, mailClient, dohEnabled, betaAccess))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateAllHeartbeatMetricPermutations() []proton.ObservabilityMetric {
|
||||||
|
planValues := []string{
|
||||||
|
planUnknown,
|
||||||
|
planOther,
|
||||||
|
planBusiness,
|
||||||
|
planIndividual,
|
||||||
|
planGroup}
|
||||||
|
mailClientValues := []string{
|
||||||
|
emailAgentAppleMail,
|
||||||
|
emailAgentOutlook,
|
||||||
|
emailAgentThunderbird,
|
||||||
|
emailAgentOther,
|
||||||
|
emailAgentUnknown,
|
||||||
|
}
|
||||||
|
enabledValues := []string{
|
||||||
|
getEnabled(true), getEnabled(false),
|
||||||
|
}
|
||||||
|
|
||||||
|
trueFalseValues := []string{
|
||||||
|
"true", "false",
|
||||||
|
}
|
||||||
|
|
||||||
|
var metrics []proton.ObservabilityMetric
|
||||||
|
for _, plan := range planValues {
|
||||||
|
for _, mailClient := range mailClientValues {
|
||||||
|
for _, dohEnabled := range enabledValues {
|
||||||
|
for _, betaAccess := range enabledValues {
|
||||||
|
for _, receivedOtherError := range trueFalseValues {
|
||||||
|
for _, receivedSyncError := range trueFalseValues {
|
||||||
|
for _, receivedEventLoopError := range trueFalseValues {
|
||||||
|
metrics = append(metrics,
|
||||||
|
generateHeartbeatMetric(plan,
|
||||||
|
mailClient,
|
||||||
|
dohEnabled,
|
||||||
|
betaAccess,
|
||||||
|
receivedOtherError,
|
||||||
|
receivedSyncError,
|
||||||
|
receivedEventLoopError,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return metrics
|
||||||
|
}
|
||||||
68
internal/services/observability/utils.go
Normal file
68
internal/services/observability/utils.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package observability
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||||
|
)
|
||||||
|
|
||||||
|
// settingsGetter - interface that maps to bridge object methods such that we
|
||||||
|
// can pass the whole object instead of individual function callbacks.
|
||||||
|
type settingsGetter interface {
|
||||||
|
GetCurrentUserAgent() string
|
||||||
|
GetProxyAllowed() bool
|
||||||
|
GetUpdateChannel() updater.Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
// User agent mapping.
|
||||||
|
const (
|
||||||
|
emailAgentAppleMail = "apple_mail"
|
||||||
|
emailAgentOutlook = "outlook"
|
||||||
|
emailAgentThunderbird = "thunderbird"
|
||||||
|
emailAgentOther = "other"
|
||||||
|
emailAgentUnknown = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
func matchUserAgent(userAgent string) string {
|
||||||
|
if userAgent == "" {
|
||||||
|
return emailAgentUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
userAgent = strings.ToLower(userAgent)
|
||||||
|
switch {
|
||||||
|
case strings.Contains(userAgent, "outlook"):
|
||||||
|
return emailAgentOutlook
|
||||||
|
case strings.Contains(userAgent, "thunderbird"):
|
||||||
|
return emailAgentThunderbird
|
||||||
|
case strings.Contains(userAgent, "mac") && strings.Contains(userAgent, "mail"):
|
||||||
|
return emailAgentAppleMail
|
||||||
|
case strings.Contains(userAgent, "mac") && strings.Contains(userAgent, "notes"):
|
||||||
|
return emailAgentUnknown
|
||||||
|
default:
|
||||||
|
return emailAgentOther
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnabled(value bool) string {
|
||||||
|
if !value {
|
||||||
|
return "disabled"
|
||||||
|
}
|
||||||
|
return "enabled"
|
||||||
|
}
|
||||||
111
internal/services/observability/utils_test.go
Normal file
111
internal/services/observability/utils_test.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package observability
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMatchUserAgent(t *testing.T) {
|
||||||
|
type agentParseResult struct {
|
||||||
|
agent string
|
||||||
|
result string
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []agentParseResult{
|
||||||
|
{
|
||||||
|
agent: "Microsoft Outlook/16.0.17928.20114 (Windows 11 Version 23H2)",
|
||||||
|
result: emailAgentOutlook,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: "Mailbird/3.0.18.0 (Windows 11 Version 23H2)",
|
||||||
|
result: emailAgentOther,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: "Microsoft Outlook/16.0.17830.20166 (Windows 11 Version 23H2)",
|
||||||
|
result: emailAgentOutlook,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: "Mac OS X Mail/16.0-3776.700.51 (macOS 14.6)",
|
||||||
|
result: emailAgentAppleMail,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: "/ (Windows 11 Version 23H2)",
|
||||||
|
result: emailAgentOther,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: "Microsoft Outlook for Mac/16.88.0-BUILDDAY (macOS 14.6)",
|
||||||
|
result: emailAgentOutlook,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: "/ (macOS 14.5)",
|
||||||
|
result: emailAgentOther,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: "/ (Freedesktop SDK 23.08 (Flatpak runtime))",
|
||||||
|
result: emailAgentOther,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: "Mac OS X Mail/16.0-3774.600.62 (macOS 14.5)",
|
||||||
|
result: emailAgentAppleMail,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: "Mac OS X Notes/4.11-2817 (macOS 14.6)",
|
||||||
|
result: emailAgentUnknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: "NoClient/0.0.1 (macOS 14.6)",
|
||||||
|
result: emailAgentOther,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: "Thunderbird/115.15.0 (Ubuntu 20.04.6 LTS)",
|
||||||
|
result: emailAgentThunderbird,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: "Thunderbird/115.14.0 (macOS 14.6)",
|
||||||
|
result: emailAgentThunderbird,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: "Thunderbird/115.10.2 (Windows 11 Version 23H2)",
|
||||||
|
result: emailAgentThunderbird,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: "Mac OS X Notes/4.9-1965 (macOS Monterey (12.0))",
|
||||||
|
result: emailAgentUnknown,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: " Thunderbird/115.14.0 (macOS 14.6) ",
|
||||||
|
result: emailAgentThunderbird,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
agent: "",
|
||||||
|
result: emailAgentUnknown,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
require.Equal(t, testCase.result, matchUserAgent(testCase.agent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatBool(t *testing.T) {
|
||||||
|
require.Equal(t, "false", formatBool(false))
|
||||||
|
require.Equal(t, "true", formatBool(true))
|
||||||
|
}
|
||||||
@ -30,7 +30,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
|
||||||
"github.com/ProtonMail/gluon/rfc5322"
|
"github.com/ProtonMail/gluon/rfc5322"
|
||||||
"github.com/ProtonMail/gluon/rfc822"
|
"github.com/ProtonMail/gluon/rfc822"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
@ -197,13 +196,7 @@ func (s *Service) sendWithKey(
|
|||||||
}
|
}
|
||||||
parentID, draftsToDelete, err := getParentID(ctx, s.client, authAddrID, addrMode, references)
|
parentID, draftsToDelete, err := getParentID(ctx, s.client, authAddrID, addrMode, references)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err := s.reporter.ReportMessageWithContext("Failed to get parent ID", reporter.Context{
|
// Sentry event has been removed; should be replaced with observability - BRIDGE-206.
|
||||||
"error": err,
|
|
||||||
"references": message.References,
|
|
||||||
}); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to report error")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.log.WithError(err).Warn("Failed to get parent ID")
|
s.log.WithError(err).Warn("Failed to get parent ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -160,10 +160,6 @@ func (s *childJob) onError(err error) {
|
|||||||
s.job.onError(err)
|
s.job.onError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *childJob) userID() string {
|
|
||||||
return s.job.userID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *childJob) chunkDivide(chunks [][]proton.FullMessage) []childJob {
|
func (s *childJob) chunkDivide(chunks [][]proton.FullMessage) []childJob {
|
||||||
numChunks := len(chunks)
|
numChunks := len(chunks)
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,67 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package observabilitymetrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
errorCaseSchemaName = "bridge_sync_message_build_errors_total"
|
||||||
|
errorCaseSchemaVersion = 1
|
||||||
|
successCaseSchemaName = "bridge_sync_message_build_success_total"
|
||||||
|
successCaseSchemaVersion = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateStageBuildFailureObservabilityMetric(errorType string) proton.ObservabilityMetric {
|
||||||
|
return proton.ObservabilityMetric{
|
||||||
|
Name: errorCaseSchemaName,
|
||||||
|
Version: errorCaseSchemaVersion,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"Value": 1,
|
||||||
|
"Labels": map[string]string{
|
||||||
|
"errorType": errorType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateNoUnlockedKeyringMetric() proton.ObservabilityMetric {
|
||||||
|
return generateStageBuildFailureObservabilityMetric("noUnlockedKeyring")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateFailedToBuildMetric() proton.ObservabilityMetric {
|
||||||
|
return generateStageBuildFailureObservabilityMetric("failedToBuild")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateMessageBuiltSuccessMetric - Maybe this is incorrect, I'm not sure how metrics with no labels
|
||||||
|
// should be dealt with. The integration tests will tell us.
|
||||||
|
func GenerateMessageBuiltSuccessMetric() proton.ObservabilityMetric {
|
||||||
|
return proton.ObservabilityMetric{
|
||||||
|
Name: successCaseSchemaName,
|
||||||
|
Version: successCaseSchemaVersion,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"Value": 1,
|
||||||
|
"Labels": map[string]string{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,7 +21,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service which mediates IMAP syncing in Bridge.
|
// Service which mediates IMAP syncing in Bridge.
|
||||||
@ -36,8 +36,9 @@ type Service struct {
|
|||||||
group *async.Group
|
group *async.Group
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(reporter reporter.Reporter,
|
func NewService(
|
||||||
panicHandler async.PanicHandler,
|
panicHandler async.PanicHandler,
|
||||||
|
observabilitySender observability.Sender,
|
||||||
) *Service {
|
) *Service {
|
||||||
limits := newSyncLimits(2 * Gigabyte)
|
limits := newSyncLimits(2 * Gigabyte)
|
||||||
|
|
||||||
@ -50,7 +51,7 @@ func NewService(reporter reporter.Reporter,
|
|||||||
limits: limits,
|
limits: limits,
|
||||||
metadataStage: NewMetadataStage(metaCh, downloadCh, limits.DownloadRequestMem, panicHandler),
|
metadataStage: NewMetadataStage(metaCh, downloadCh, limits.DownloadRequestMem, panicHandler),
|
||||||
downloadStage: NewDownloadStage(downloadCh, buildCh, limits.MaxParallelDownloads, panicHandler),
|
downloadStage: NewDownloadStage(downloadCh, buildCh, limits.MaxParallelDownloads, panicHandler),
|
||||||
buildStage: NewBuildStage(buildCh, applyCh, limits.MessageBuildMem, panicHandler, reporter),
|
buildStage: NewBuildStage(buildCh, applyCh, limits.MessageBuildMem, panicHandler, observabilitySender),
|
||||||
applyStage: NewApplyStage(applyCh),
|
applyStage: NewApplyStage(applyCh),
|
||||||
metaCh: metaCh,
|
metaCh: metaCh,
|
||||||
group: async.NewGroup(context.Background(), panicHandler),
|
group: async.NewGroup(context.Background(), panicHandler),
|
||||||
|
|||||||
@ -26,9 +26,10 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/gluon/logging"
|
"github.com/ProtonMail/gluon/logging"
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
|
obsMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics"
|
||||||
"github.com/bradenaw/juniper/parallel"
|
"github.com/bradenaw/juniper/parallel"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -50,8 +51,10 @@ type BuildStage struct {
|
|||||||
maxBuildMem uint64
|
maxBuildMem uint64
|
||||||
|
|
||||||
panicHandler async.PanicHandler
|
panicHandler async.PanicHandler
|
||||||
reporter reporter.Reporter
|
|
||||||
log *logrus.Entry
|
log *logrus.Entry
|
||||||
|
|
||||||
|
// Observability
|
||||||
|
observabilitySender observability.Sender
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBuildStage(
|
func NewBuildStage(
|
||||||
@ -59,7 +62,7 @@ func NewBuildStage(
|
|||||||
output BuildStageOutput,
|
output BuildStageOutput,
|
||||||
maxBuildMem uint64,
|
maxBuildMem uint64,
|
||||||
panicHandler async.PanicHandler,
|
panicHandler async.PanicHandler,
|
||||||
reporter reporter.Reporter,
|
observabilitySender observability.Sender,
|
||||||
) *BuildStage {
|
) *BuildStage {
|
||||||
return &BuildStage{
|
return &BuildStage{
|
||||||
input: input,
|
input: input,
|
||||||
@ -67,7 +70,7 @@ func NewBuildStage(
|
|||||||
maxBuildMem: maxBuildMem,
|
maxBuildMem: maxBuildMem,
|
||||||
log: logrus.WithField("sync-stage", "build"),
|
log: logrus.WithField("sync-stage", "build"),
|
||||||
panicHandler: panicHandler,
|
panicHandler: panicHandler,
|
||||||
reporter: reporter,
|
observabilitySender: observabilitySender,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,35 +150,24 @@ func (b *BuildStage) run(ctx context.Context) {
|
|||||||
req.job.log.WithError(err).Error("Failed to add failed message ID")
|
req.job.log.WithError(err).Error("Failed to add failed message ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := b.reporter.ReportMessageWithContext("Failed to build message - no unlocked keyring (sync)", reporter.Context{
|
b.observabilitySender.AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateNoUnlockedKeyringMetric())
|
||||||
"messageID": msg.ID,
|
|
||||||
"userID": req.userID(),
|
|
||||||
}); err != nil {
|
|
||||||
req.job.log.WithError(err).Error("Failed to report message build error")
|
|
||||||
}
|
|
||||||
return BuildResult{}, nil
|
return BuildResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := req.job.messageBuilder.BuildMessage(req.job.labels, msg, kr, new(bytes.Buffer))
|
res, err := req.job.messageBuilder.BuildMessage(req.job.labels, msg, kr, new(bytes.Buffer))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
req.job.log.WithError(err).WithField("msgID", msg.ID).Error("Failed to build message (syn)")
|
req.job.log.WithError(err).WithField("msgID", msg.ID).Error("Failed to build message (sync)")
|
||||||
|
|
||||||
if err := req.job.state.AddFailedMessageID(req.getContext(), msg.ID); err != nil {
|
if err := req.job.state.AddFailedMessageID(req.getContext(), msg.ID); err != nil {
|
||||||
req.job.log.WithError(err).Error("Failed to add failed message ID")
|
req.job.log.WithError(err).Error("Failed to add failed message ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := b.reporter.ReportMessageWithContext("Failed to build message (sync)", reporter.Context{
|
b.observabilitySender.AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateFailedToBuildMetric())
|
||||||
"messageID": msg.ID,
|
|
||||||
"error": err,
|
|
||||||
"userID": req.userID(),
|
|
||||||
}); err != nil {
|
|
||||||
req.job.log.WithError(err).Error("Failed to report message build error")
|
|
||||||
}
|
|
||||||
|
|
||||||
// We could sync a placeholder message here, but for now we skip it entirely.
|
// We could sync a placeholder message here, but for now we skip it entirely.
|
||||||
return BuildResult{}, nil
|
return BuildResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.observabilitySender.AddMetrics(obsMetrics.GenerateMessageBuiltSuccessMetric())
|
||||||
return res, nil
|
return res, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -24,10 +24,11 @@ import (
|
|||||||
|
|
||||||
"github.com/ProtonMail/gluon/async"
|
"github.com/ProtonMail/gluon/async"
|
||||||
"github.com/ProtonMail/gluon/imap"
|
"github.com/ProtonMail/gluon/imap"
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge/mocks"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
|
obsMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics"
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -67,7 +68,6 @@ func TestBuildStage_SuccessRemovesFailedMessage(t *testing.T) {
|
|||||||
|
|
||||||
input := NewChannelConsumerProducer[BuildRequest]()
|
input := NewChannelConsumerProducer[BuildRequest]()
|
||||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||||
reporter := mocks.NewMockReporter(mockCtrl)
|
|
||||||
|
|
||||||
labels := getTestLabels()
|
labels := getTestLabels()
|
||||||
|
|
||||||
@ -105,7 +105,10 @@ func TestBuildStage_SuccessRemovesFailedMessage(t *testing.T) {
|
|||||||
tj.messageBuilder.EXPECT().BuildMessage(gomock.Eq(labels), gomock.Eq(msg), gomock.Any(), gomock.Any()).Return(buildResult, nil)
|
tj.messageBuilder.EXPECT().BuildMessage(gomock.Eq(labels), gomock.Eq(msg), gomock.Any(), gomock.Any()).Return(buildResult, nil)
|
||||||
tj.state.EXPECT().RemFailedMessageID(gomock.Any(), gomock.Eq("MSG"))
|
tj.state.EXPECT().RemFailedMessageID(gomock.Any(), gomock.Eq("MSG"))
|
||||||
|
|
||||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, reporter)
|
observabilityService := mocks.NewMockObservabilitySender(mockCtrl)
|
||||||
|
observabilityService.EXPECT().AddMetrics(obsMetrics.GenerateMessageBuiltSuccessMetric())
|
||||||
|
|
||||||
|
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, observabilityService)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
stage.run(ctx)
|
stage.run(ctx)
|
||||||
@ -125,7 +128,7 @@ func TestBuildStage_BuildFailureIsReportedButDoesNotCancelJob(t *testing.T) {
|
|||||||
|
|
||||||
input := NewChannelConsumerProducer[BuildRequest]()
|
input := NewChannelConsumerProducer[BuildRequest]()
|
||||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||||
mockReporter := mocks.NewMockReporter(mockCtrl)
|
mockObservabilityService := mocks.NewMockObservabilitySender(mockCtrl)
|
||||||
|
|
||||||
labels := getTestLabels()
|
labels := getTestLabels()
|
||||||
|
|
||||||
@ -156,15 +159,12 @@ func TestBuildStage_BuildFailureIsReportedButDoesNotCancelJob(t *testing.T) {
|
|||||||
|
|
||||||
tj.messageBuilder.EXPECT().BuildMessage(gomock.Eq(labels), gomock.Eq(msg), gomock.Any(), gomock.Any()).Return(BuildResult{}, buildError)
|
tj.messageBuilder.EXPECT().BuildMessage(gomock.Eq(labels), gomock.Eq(msg), gomock.Any(), gomock.Any()).Return(BuildResult{}, buildError)
|
||||||
tj.state.EXPECT().AddFailedMessageID(gomock.Any(), gomock.Eq([]string{"MSG"}))
|
tj.state.EXPECT().AddFailedMessageID(gomock.Any(), gomock.Eq([]string{"MSG"}))
|
||||||
mockReporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Eq(reporter.Context{
|
|
||||||
"userID": "u",
|
|
||||||
"messageID": "MSG",
|
|
||||||
"error": buildError,
|
|
||||||
})).Return(nil)
|
|
||||||
|
|
||||||
tj.syncReporter.EXPECT().OnProgress(gomock.Any(), gomock.Eq(int64(10)))
|
tj.syncReporter.EXPECT().OnProgress(gomock.Any(), gomock.Eq(int64(10)))
|
||||||
|
|
||||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
|
mockObservabilityService.EXPECT().AddDistinctMetrics(observability.SyncError, obsMetrics.GenerateNoUnlockedKeyringMetric())
|
||||||
|
|
||||||
|
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockObservabilityService)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
stage.run(ctx)
|
stage.run(ctx)
|
||||||
@ -183,7 +183,6 @@ func TestBuildStage_FailedToLocateKeyRingIsReportedButDoesNotFailBuild(t *testin
|
|||||||
|
|
||||||
input := NewChannelConsumerProducer[BuildRequest]()
|
input := NewChannelConsumerProducer[BuildRequest]()
|
||||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||||
mockReporter := mocks.NewMockReporter(mockCtrl)
|
|
||||||
|
|
||||||
labels := getTestLabels()
|
labels := getTestLabels()
|
||||||
|
|
||||||
@ -209,14 +208,13 @@ func TestBuildStage_FailedToLocateKeyRingIsReportedButDoesNotFailBuild(t *testin
|
|||||||
tj.job.end()
|
tj.job.end()
|
||||||
|
|
||||||
tj.state.EXPECT().AddFailedMessageID(gomock.Any(), gomock.Eq([]string{"MSG"}))
|
tj.state.EXPECT().AddFailedMessageID(gomock.Any(), gomock.Eq([]string{"MSG"}))
|
||||||
mockReporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Eq(reporter.Context{
|
|
||||||
"userID": "u",
|
|
||||||
"messageID": "MSG",
|
|
||||||
})).Return(nil)
|
|
||||||
|
|
||||||
tj.syncReporter.EXPECT().OnProgress(gomock.Any(), gomock.Eq(int64(10)))
|
tj.syncReporter.EXPECT().OnProgress(gomock.Any(), gomock.Eq(int64(10)))
|
||||||
|
|
||||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
|
observabilitySender := mocks.NewMockObservabilitySender(mockCtrl)
|
||||||
|
observabilitySender.EXPECT().AddDistinctMetrics(observability.SyncError)
|
||||||
|
|
||||||
|
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, observabilitySender)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
stage.run(ctx)
|
stage.run(ctx)
|
||||||
@ -235,7 +233,6 @@ func TestBuildStage_OtherErrorsFailJob(t *testing.T) {
|
|||||||
|
|
||||||
input := NewChannelConsumerProducer[BuildRequest]()
|
input := NewChannelConsumerProducer[BuildRequest]()
|
||||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||||
mockReporter := mocks.NewMockReporter(mockCtrl)
|
|
||||||
|
|
||||||
labels := getTestLabels()
|
labels := getTestLabels()
|
||||||
|
|
||||||
@ -261,7 +258,7 @@ func TestBuildStage_OtherErrorsFailJob(t *testing.T) {
|
|||||||
childJob := tj.job.newChildJob("f", 10)
|
childJob := tj.job.newChildJob("f", 10)
|
||||||
tj.job.end()
|
tj.job.end()
|
||||||
|
|
||||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
|
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mocks.NewMockObservabilitySender(mockCtrl))
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
stage.run(ctx)
|
stage.run(ctx)
|
||||||
@ -283,7 +280,6 @@ func TestBuildStage_CancelledJobIsDiscarded(t *testing.T) {
|
|||||||
|
|
||||||
input := NewChannelConsumerProducer[BuildRequest]()
|
input := NewChannelConsumerProducer[BuildRequest]()
|
||||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||||
mockReporter := mocks.NewMockReporter(mockCtrl)
|
|
||||||
|
|
||||||
msg := proton.FullMessage{
|
msg := proton.FullMessage{
|
||||||
Message: proton.Message{
|
Message: proton.Message{
|
||||||
@ -294,7 +290,7 @@ func TestBuildStage_CancelledJobIsDiscarded(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mockReporter)
|
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mocks.NewMockObservabilitySender(mockCtrl))
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
@ -327,7 +323,6 @@ func TestTask_EmptyInputDoesNotCrash(t *testing.T) {
|
|||||||
|
|
||||||
input := NewChannelConsumerProducer[BuildRequest]()
|
input := NewChannelConsumerProducer[BuildRequest]()
|
||||||
output := NewChannelConsumerProducer[ApplyRequest]()
|
output := NewChannelConsumerProducer[ApplyRequest]()
|
||||||
reporter := mocks.NewMockReporter(mockCtrl)
|
|
||||||
|
|
||||||
labels := getTestLabels()
|
labels := getTestLabels()
|
||||||
|
|
||||||
@ -340,7 +335,7 @@ func TestTask_EmptyInputDoesNotCrash(t *testing.T) {
|
|||||||
childJob := tj.job.newChildJob("f", 10)
|
childJob := tj.job.newChildJob("f", 10)
|
||||||
tj.job.end()
|
tj.job.end()
|
||||||
|
|
||||||
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, reporter)
|
stage := NewBuildStage(input, output, 1024, &async.NoopPanicHandler{}, mocks.NewMockObservabilitySender(mockCtrl))
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
stage.run(ctx)
|
stage.run(ctx)
|
||||||
|
|||||||
@ -111,7 +111,7 @@ func (s *Service) readCacheFile() {
|
|||||||
|
|
||||||
file, err := os.Open(s.cacheFilepath)
|
file, err := os.Open(s.cacheFilepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.WithError(err).Error("Unable to open cache file")
|
s.log.WithError(err).Info("Unable to open cache file")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -101,7 +101,7 @@ func (u *Updater) InstallUpdate(ctx context.Context, downloader Downloader, upda
|
|||||||
update.Package+".sig",
|
update.Package+".sig",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrDownloadVerify
|
return fmt.Errorf("%w: %w", ErrDownloadVerify, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := u.installer.InstallUpdate(update.Version, bytes.NewReader(b)); err != nil {
|
if err := u.installer.InstallUpdate(update.Version, bytes.NewReader(b)); err != nil {
|
||||||
|
|||||||
@ -115,7 +115,6 @@ func New(
|
|||||||
isNew bool,
|
isNew bool,
|
||||||
notificationStore *notifications.Store,
|
notificationStore *notifications.Store,
|
||||||
getFlagValFn unleash.GetFlagValueFn,
|
getFlagValFn unleash.GetFlagValueFn,
|
||||||
pushObservabilityMetric observability.PushObsMetricFn,
|
|
||||||
) (*User, error) {
|
) (*User, error) {
|
||||||
user, err := newImpl(
|
user, err := newImpl(
|
||||||
ctx,
|
ctx,
|
||||||
@ -137,7 +136,6 @@ func New(
|
|||||||
isNew,
|
isNew,
|
||||||
notificationStore,
|
notificationStore,
|
||||||
getFlagValFn,
|
getFlagValFn,
|
||||||
pushObservabilityMetric,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Cleanup any pending resources on error
|
// Cleanup any pending resources on error
|
||||||
@ -172,7 +170,6 @@ func newImpl(
|
|||||||
isNew bool,
|
isNew bool,
|
||||||
notificationStore *notifications.Store,
|
notificationStore *notifications.Store,
|
||||||
getFlagValueFn unleash.GetFlagValueFn,
|
getFlagValueFn unleash.GetFlagValueFn,
|
||||||
pushObservabilityMetric observability.PushObsMetricFn,
|
|
||||||
) (*User, error) {
|
) (*User, error) {
|
||||||
logrus.WithField("userID", apiUser.ID).Info("Creating new user")
|
logrus.WithField("userID", apiUser.ID).Info("Creating new user")
|
||||||
|
|
||||||
@ -288,9 +285,10 @@ func newImpl(
|
|||||||
syncConfigDir,
|
syncConfigDir,
|
||||||
user.maxSyncMemory,
|
user.maxSyncMemory,
|
||||||
showAllMail,
|
showAllMail,
|
||||||
|
observabilityService,
|
||||||
)
|
)
|
||||||
|
|
||||||
user.notificationService = notifications.NewService(user.id, user.eventService, user, notificationStore, getFlagValueFn, pushObservabilityMetric)
|
user.notificationService = notifications.NewService(user.id, user.eventService, user, notificationStore, getFlagValueFn, observabilityService)
|
||||||
|
|
||||||
// Check for status_progress when triggered.
|
// Check for status_progress when triggered.
|
||||||
user.goStatusProgress = user.tasks.PeriodicOrTrigger(configstatus.ProgressCheckInterval, 0, func(ctx context.Context) {
|
user.goStatusProgress = user.tasks.PeriodicOrTrigger(configstatus.ProgressCheckInterval, 0, func(ctx context.Context) {
|
||||||
|
|||||||
@ -175,7 +175,6 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
|
|||||||
func(_ string) bool {
|
func(_ string) bool {
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
func(_ proton.ObservabilityMetric) {},
|
|
||||||
)
|
)
|
||||||
require.NoError(tb, err)
|
require.NoError(tb, err)
|
||||||
defer user.Close()
|
defer user.Close()
|
||||||
|
|||||||
@ -19,6 +19,7 @@ package vault
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
@ -37,6 +38,10 @@ func (user *User) Username() string {
|
|||||||
return user.vault.getUser(user.userID).Username
|
return user.vault.getUser(user.userID).Username
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *User) usernameUnsafe() string {
|
||||||
|
return user.vault.getUserUnsafe(user.userID).Username
|
||||||
|
}
|
||||||
|
|
||||||
// PrimaryEmail returns the user's primary email address.
|
// PrimaryEmail returns the user's primary email address.
|
||||||
func (user *User) PrimaryEmail() string {
|
func (user *User) PrimaryEmail() string {
|
||||||
return user.vault.getUser(user.userID).PrimaryEmail
|
return user.vault.getUser(user.userID).PrimaryEmail
|
||||||
@ -242,3 +247,15 @@ func (user *User) SetShouldSync(shouldResync bool) error {
|
|||||||
func (user *User) GetShouldResync() bool {
|
func (user *User) GetShouldResync() bool {
|
||||||
return user.vault.getUser(user.userID).ShouldResync
|
return user.vault.getUser(user.userID).ShouldResync
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateUsernameUnsafe - updates the username of the relevant user, provided that the new username is not empty
|
||||||
|
// and differs from the previous. Writes are not performed if this case is not met.
|
||||||
|
// Should only be called from contexts where the vault mutex is already locked.
|
||||||
|
func (user *User) updateUsernameUnsafe(username string) error {
|
||||||
|
if strings.TrimSpace(username) == "" || user.usernameUnsafe() == username {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return user.vault.modUserUnsafe(user.userID, func(userData *UserData) {
|
||||||
|
userData.Username = username
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -240,6 +240,10 @@ func (vault *Vault) GetOrAddUser(userID, username, primaryEmail, authUID, authRe
|
|||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := user.updateUsernameUnsafe(username); err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
return user, false, nil
|
return user, false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -450,6 +454,22 @@ func (vault *Vault) getUser(userID string) UserData {
|
|||||||
return users[idx]
|
return users[idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getUserUnsafe - fetches the relevant UserData.
|
||||||
|
// Should only be called from contexts in which the vault mutex has been read locked.
|
||||||
|
func (vault *Vault) getUserUnsafe(userID string) UserData {
|
||||||
|
users := vault.getUnsafe().Users
|
||||||
|
|
||||||
|
idx := xslices.IndexFunc(users, func(user UserData) bool {
|
||||||
|
return user.UserID == userID
|
||||||
|
})
|
||||||
|
|
||||||
|
if idx < 0 {
|
||||||
|
panic("Unknown user")
|
||||||
|
}
|
||||||
|
|
||||||
|
return users[idx]
|
||||||
|
}
|
||||||
|
|
||||||
func (vault *Vault) modUser(userID string, fn func(userData *UserData)) error {
|
func (vault *Vault) modUser(userID string, fn func(userData *UserData)) error {
|
||||||
vault.lock.Lock()
|
vault.lock.Lock()
|
||||||
defer vault.lock.Unlock()
|
defer vault.lock.Unlock()
|
||||||
|
|||||||
@ -857,6 +857,23 @@ func getFileReader(filename string) io.Reader {
|
|||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseInvalidOriginalBoundary(t *testing.T) {
|
||||||
|
f := getFileReader("incorrect_boundary_w_invalid_character_tuta.eml")
|
||||||
|
|
||||||
|
p, err := parser.New(f)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, true, p.Root().Header.Get("Content-Type") == `multipart/related; boundary="------------1234567890@tutanota"`)
|
||||||
|
|
||||||
|
m, err := ParseWithParser(p, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, true, strings.HasPrefix(string(m.MIMEBody), "Content-Type: multipart/related;\r\n boundary="))
|
||||||
|
require.Equal(t, false, strings.HasPrefix(string(m.MIMEBody), `Content-Type: multipart/related;\n boundary="------------1234567890@tutanota"`))
|
||||||
|
require.Equal(t, false, strings.HasPrefix(string(m.MIMEBody), `Content-Type: multipart/related;\n boundary=------------1234567890@tutanota`))
|
||||||
|
require.Equal(t, false, strings.HasPrefix(string(m.MIMEBody), `Content-Type: multipart/related;\n boundary=1234567890@tutanota`))
|
||||||
|
}
|
||||||
|
|
||||||
type panicReader struct{}
|
type panicReader struct{}
|
||||||
|
|
||||||
func (panicReader) Read(_ []byte) (int, error) {
|
func (panicReader) Read(_ []byte) (int, error) {
|
||||||
|
|||||||
13
pkg/message/testdata/incorrect_boundary_w_invalid_character_tuta.eml
vendored
Normal file
13
pkg/message/testdata/incorrect_boundary_w_invalid_character_tuta.eml
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
Date: Mon, 01 Jan 2000 00:00:00 +0000 (UTC)
|
||||||
|
From: Daniel at Test <daniel@test.com>
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Subject: Test incorrect original boundary w. invalid character
|
||||||
|
To: david@test.com
|
||||||
|
Content-Type: multipart/related; boundary="------------1234567890@tutanota"
|
||||||
|
|
||||||
|
--------------1234567890@tutanota
|
||||||
|
Content-Type: text/html; charset=UTF-8
|
||||||
|
Content-transfer-encoding: base64
|
||||||
|
|
||||||
|
PGh0bWw+PGgxPkhlbGxvIFdvcmxkITwvaDE+PC9odG1sPg==
|
||||||
|
--------------1234567890@tutanota--
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
|
<Platforms>AnyCPU;x64</Platforms>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="FlaUI.Core" Version="4.0.0" />
|
||||||
|
<PackageReference Include="FlaUI.UIA3" Version="4.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
|
||||||
|
<PackageReference Include="NUnit" Version="4.2.1" />
|
||||||
|
<PackageReference Include="NUnit.Analyzers" Version="4.3.0">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||||
|
<PackageReference Include="System.Drawing.Common" Version="8.0.8" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="NUnit.Framework" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
29
tests/e2e/ui_tests/windows_os/Results/HomeResult.cs
Normal file
29
tests/e2e/ui_tests/windows_os/Results/HomeResult.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
using FlaUI.Core.AutomationElements;
|
||||||
|
using FlaUI.Core.Definitions;
|
||||||
|
|
||||||
|
namespace ProtonMailBridge.UI.Tests.Results
|
||||||
|
{
|
||||||
|
public class HomeResult : UIActions
|
||||||
|
{
|
||||||
|
private Button SignOutButton => AccountView.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Sign out"))).AsButton();
|
||||||
|
private AutomationElement NotificationWindow => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window));
|
||||||
|
private TextBox FreeAccountErrorText => NotificationWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text)).AsTextBox();
|
||||||
|
private TextBox SignedOutAccount => AccountView.FindFirstDescendant(cf => cf.ByControlType(ControlType.Text)).AsTextBox();
|
||||||
|
public HomeResult CheckIfLoggedIn()
|
||||||
|
{
|
||||||
|
Assert.That(SignOutButton.IsAvailable, Is.True);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HomeResult CheckIfFreeAccountErrorIsDisplayed(string ErrorText)
|
||||||
|
{
|
||||||
|
Assert.That(FreeAccountErrorText.Name == ErrorText, Is.True);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public HomeResult CheckIfAccountIsSignedOut()
|
||||||
|
{
|
||||||
|
Assert.That(SignedOutAccount.IsAvailable, Is.True);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
tests/e2e/ui_tests/windows_os/TestSession.cs
Normal file
43
tests/e2e/ui_tests/windows_os/TestSession.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using FlaUI.Core.AutomationElements;
|
||||||
|
using FlaUI.Core;
|
||||||
|
using FlaUI.UIA3;
|
||||||
|
using ProtonMailBridge.UI.Tests.TestsHelper;
|
||||||
|
using FlaUI.Core.Input;
|
||||||
|
|
||||||
|
namespace ProtonMailBridge.UI.Tests
|
||||||
|
{
|
||||||
|
public class TestSession
|
||||||
|
{
|
||||||
|
|
||||||
|
public static Application App;
|
||||||
|
protected static Application Service;
|
||||||
|
protected static Window Window;
|
||||||
|
|
||||||
|
protected static void ClientCleanup()
|
||||||
|
{
|
||||||
|
App.Kill();
|
||||||
|
App.Dispose();
|
||||||
|
// Give some time to properly exit the app
|
||||||
|
Thread.Sleep(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void LaunchApp()
|
||||||
|
{
|
||||||
|
string appExecutable = TestData.AppExecutable;
|
||||||
|
Application.Launch(appExecutable);
|
||||||
|
Wait.UntilInputIsProcessed(TestData.FiveSecondsTimeout);
|
||||||
|
App = Application.Attach("bridge-gui.exe");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Window = App.GetMainWindow(new UIA3Automation(), TestData.ThirtySecondsTimeout);
|
||||||
|
}
|
||||||
|
catch (System.TimeoutException)
|
||||||
|
{
|
||||||
|
Assert.Fail("Failed to get window of application!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
tests/e2e/ui_tests/windows_os/Tests/LoginLogoutTests.cs
Normal file
51
tests/e2e/ui_tests/windows_os/Tests/LoginLogoutTests.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
using NUnit.Framework;
|
||||||
|
using ProtonMailBridge.UI.Tests.TestsHelper;
|
||||||
|
using ProtonMailBridge.UI.Tests.Windows;
|
||||||
|
using ProtonMailBridge.UI.Tests.Results;
|
||||||
|
|
||||||
|
namespace ProtonMailBridge.UI.Tests.Tests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class LoginLogoutTests : TestSession
|
||||||
|
{
|
||||||
|
private readonly LoginWindow _loginWindow = new();
|
||||||
|
private readonly HomeWindow _mainWindow = new();
|
||||||
|
private readonly HomeResult _homeResult = new();
|
||||||
|
private readonly string FreeAccountErrorText = "Bridge is exclusive to our mail paid plans. Upgrade your account to use Bridge.";
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void LoginAsPaidUser()
|
||||||
|
{
|
||||||
|
_loginWindow.SignIn(TestUserData.GetPaidUser());
|
||||||
|
_homeResult.CheckIfLoggedIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void LoginAsFreeUser()
|
||||||
|
{
|
||||||
|
_loginWindow.SignIn(TestUserData.GetFreeUser());
|
||||||
|
_homeResult.CheckIfFreeAccountErrorIsDisplayed(FreeAccountErrorText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SuccessfullLogout()
|
||||||
|
{
|
||||||
|
_loginWindow.SignIn(TestUserData.GetPaidUser());
|
||||||
|
_mainWindow.SignOutAccount();
|
||||||
|
_homeResult.CheckIfAccountIsSignedOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void TestInitialize()
|
||||||
|
{
|
||||||
|
LaunchApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TestCleanup()
|
||||||
|
{
|
||||||
|
_mainWindow.RemoveAccount();
|
||||||
|
ClientCleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
tests/e2e/ui_tests/windows_os/TestsHelper/TestData.cs
Normal file
16
tests/e2e/ui_tests/windows_os/TestsHelper/TestData.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace ProtonMailBridge.UI.Tests.TestsHelper
|
||||||
|
{
|
||||||
|
public static class TestData
|
||||||
|
{
|
||||||
|
public static TimeSpan FiveSecondsTimeout => TimeSpan.FromSeconds(5);
|
||||||
|
public static TimeSpan TenSecondsTimeout => TimeSpan.FromSeconds(10);
|
||||||
|
public static TimeSpan ThirtySecondsTimeout => TimeSpan.FromSeconds(30);
|
||||||
|
public static TimeSpan OneMinuteTimeout => TimeSpan.FromSeconds(60);
|
||||||
|
public static TimeSpan RetryInterval => TimeSpan.FromMilliseconds(1000);
|
||||||
|
public static string AppExecutable => "C:\\Program Files\\Proton AG\\Proton Mail Bridge\\bridge-gui.exe";
|
||||||
|
}
|
||||||
|
}
|
||||||
58
tests/e2e/ui_tests/windows_os/TestsHelper/TestUserData.cs
Normal file
58
tests/e2e/ui_tests/windows_os/TestsHelper/TestUserData.cs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace ProtonMailBridge.UI.Tests.TestsHelper
|
||||||
|
{
|
||||||
|
public class TestUserData
|
||||||
|
{
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
public TestUserData(string username, string password)
|
||||||
|
{
|
||||||
|
Username = username;
|
||||||
|
Password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TestUserData GetFreeUser()
|
||||||
|
{
|
||||||
|
(string username, string password) = GetusernameAndPassword("BRIDGE_FLAUI_FREE_USER");
|
||||||
|
return new TestUserData(username, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TestUserData GetPaidUser()
|
||||||
|
{
|
||||||
|
(string username, string password) = GetusernameAndPassword("BRIDGE_FLAUI_PAID_USER");
|
||||||
|
return new TestUserData(username, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TestUserData GetIncorrectCredentialsUser()
|
||||||
|
{
|
||||||
|
return new TestUserData("IncorrectUsername", "IncorrectPass");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string, string) GetusernameAndPassword(string userType)
|
||||||
|
{
|
||||||
|
// Get the environment variable for the user and check if missing
|
||||||
|
// When changing or adding an environment variable, you must restart Visual Studio
|
||||||
|
// if you have it open while doing this
|
||||||
|
string? str = Environment.GetEnvironmentVariable(userType);
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
{
|
||||||
|
throw new Exception($"Missing environment variable: {userType}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the environment variable contains only one ':'
|
||||||
|
// The ':' character must be between the username/email and password
|
||||||
|
string ch = ":";
|
||||||
|
if ((str.IndexOf(ch) != str.LastIndexOf(ch)) | (str.IndexOf(ch) == -1))
|
||||||
|
{
|
||||||
|
throw new Exception(
|
||||||
|
$"Environment variable {str} must contain one ':' and it must be between username and password!"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] split = str.Split(':');
|
||||||
|
return (split[0], split[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
tests/e2e/ui_tests/windows_os/UIActions.cs
Normal file
14
tests/e2e/ui_tests/windows_os/UIActions.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using FlaUI.Core.AutomationElements;
|
||||||
|
using FlaUI.Core.Definitions;
|
||||||
|
using FlaUI.Core.Input;
|
||||||
|
using FlaUI.Core.Tools;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
|
namespace ProtonMailBridge.UI.Tests
|
||||||
|
{
|
||||||
|
public class UIActions : TestSession
|
||||||
|
{
|
||||||
|
public AutomationElement AccountView => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Pane));
|
||||||
|
}
|
||||||
|
}
|
||||||
34
tests/e2e/ui_tests/windows_os/Windows/HomeWindow.cs
Normal file
34
tests/e2e/ui_tests/windows_os/Windows/HomeWindow.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using FlaUI.Core.AutomationElements;
|
||||||
|
using FlaUI.Core.Definitions;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
|
||||||
|
namespace ProtonMailBridge.UI.Tests.Windows
|
||||||
|
{
|
||||||
|
public class HomeWindow : UIActions
|
||||||
|
{
|
||||||
|
private AutomationElement[] AccountViewButtons => AccountView.FindAllChildren(cf => cf.ByControlType(ControlType.Button));
|
||||||
|
private Button RemoveAccountButton => AccountViewButtons[1].AsButton();
|
||||||
|
private AutomationElement RemoveAccountConfirmModal => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Window));
|
||||||
|
private Button ConfirmRemoveAccountButton => RemoveAccountConfirmModal.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Remove this account"))).AsButton();
|
||||||
|
private Button SignOutButton => AccountView.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Sign out"))).AsButton();
|
||||||
|
public HomeWindow RemoveAccount()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RemoveAccountButton.Click();
|
||||||
|
ConfirmRemoveAccountButton.Click();
|
||||||
|
}
|
||||||
|
catch (System.NullReferenceException)
|
||||||
|
{
|
||||||
|
ClientCleanup();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public HomeWindow SignOutAccount()
|
||||||
|
{
|
||||||
|
SignOutButton.Click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
tests/e2e/ui_tests/windows_os/Windows/LoginWindow.cs
Normal file
49
tests/e2e/ui_tests/windows_os/Windows/LoginWindow.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
using FlaUI.Core.AutomationElements;
|
||||||
|
using FlaUI.Core.Input;
|
||||||
|
using FlaUI.Core.Definitions;
|
||||||
|
using ProtonMailBridge.UI.Tests.TestsHelper;
|
||||||
|
|
||||||
|
namespace ProtonMailBridge.UI.Tests.Windows
|
||||||
|
{
|
||||||
|
public class LoginWindow : UIActions
|
||||||
|
{
|
||||||
|
private AutomationElement[] InputFields => Window.FindAllDescendants(cf => cf.ByControlType(ControlType.Edit));
|
||||||
|
private TextBox UsernameInput => InputFields[0].AsTextBox();
|
||||||
|
private TextBox PasswordInput => InputFields[1].AsTextBox();
|
||||||
|
private Button SignInButton => Window.FindFirstDescendant(cf => cf.ByControlType(ControlType.Button).And(cf.ByName("Sign in"))).AsButton();
|
||||||
|
private Button StartSetupButton => Window.FindFirstDescendant(cf => cf.ByName("Start setup")).AsButton();
|
||||||
|
private Button SetUpLater => Window.FindFirstDescendant(cf => cf.ByName("Setup later")).AsButton();
|
||||||
|
|
||||||
|
public LoginWindow SignIn(TestUserData user)
|
||||||
|
{
|
||||||
|
ClickStartSetupButton();
|
||||||
|
EnterCredentials(user);
|
||||||
|
Wait.UntilInputIsProcessed(TestData.TenSecondsTimeout);
|
||||||
|
SetUpLater?.Click();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoginWindow SignIn(string username, string password)
|
||||||
|
{
|
||||||
|
TestUserData user = new TestUserData(username, password);
|
||||||
|
SignIn(user);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoginWindow ClickStartSetupButton()
|
||||||
|
{
|
||||||
|
StartSetupButton?.Click();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoginWindow EnterCredentials(TestUserData user)
|
||||||
|
{
|
||||||
|
UsernameInput.Text = user.Username;
|
||||||
|
PasswordInput.Text = user.Password;
|
||||||
|
SignInButton.Click();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
tests/e2e/ui_tests/windows_os/app.config
Normal file
35
tests/e2e/ui_tests/windows_os/app.config
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<runtime>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Drawing.Common" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Diagnostics.PerformanceCounter" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Reflection.Metadata" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-8.0.0.1" newVersion="8.0.0.1"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
</runtime>
|
||||||
|
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/></startup></configuration>
|
||||||
31
tests/e2e/ui_tests/windows_os/ui_tests.sln
Normal file
31
tests/e2e/ui_tests/windows_os/ui_tests.sln
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.11.35208.52
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProtonMailBridge.UI.Tests", "ProtonMailBridge.UI.Tests.csproj", "{027E5266-E353-4095-AF24-B3ED240EACAA}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{027E5266-E353-4095-AF24-B3ED240EACAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{027E5266-E353-4095-AF24-B3ED240EACAA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{027E5266-E353-4095-AF24-B3ED240EACAA}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{027E5266-E353-4095-AF24-B3ED240EACAA}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{027E5266-E353-4095-AF24-B3ED240EACAA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{027E5266-E353-4095-AF24-B3ED240EACAA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{027E5266-E353-4095-AF24-B3ED240EACAA}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{027E5266-E353-4095-AF24-B3ED240EACAA}.Release|x64.Build.0 = Release|x64
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {817DD45A-EA2C-4F16-A680-5810DADCE4E7}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@ -57,7 +57,6 @@ Feature: IMAP create messages
|
|||||||
And IMAP client "1" eventually sees the following messages in "All Mail":
|
And IMAP client "1" eventually sees the following messages in "All Mail":
|
||||||
| from | to | subject | body |
|
| from | to | subject | body |
|
||||||
| [alias:alias]@[domain] | john.doe@email.com | foo | bar |
|
| [alias:alias]@[domain] | john.doe@email.com | foo | bar |
|
||||||
And bridge reports a message with "GODT-3185: import with non-default address in combined mode: using sender address"
|
|
||||||
|
|
||||||
Scenario: Imports an unrelated message to inbox
|
Scenario: Imports an unrelated message to inbox
|
||||||
When IMAP client "1" appends the following messages to "INBOX":
|
When IMAP client "1" appends the following messages to "INBOX":
|
||||||
|
|||||||
@ -164,7 +164,6 @@ Feature: IMAP Draft messages
|
|||||||
And IMAP client "1" eventually sees the following messages in "Drafts":
|
And IMAP client "1" eventually sees the following messages in "Drafts":
|
||||||
| to | subject | body |
|
| to | subject | body |
|
||||||
| someone@example.com | Draft without From | This is a Draft without From in header |
|
| someone@example.com | Draft without From | This is a Draft without From in header |
|
||||||
And bridge reports a message with "GODT-3185: draft with non-default invalid address in combined mode: error import/draft"
|
|
||||||
|
|
||||||
@regression
|
@regression
|
||||||
Scenario: Only one draft in Drafts and All Mail after editing it locally multiple times
|
Scenario: Only one draft in Drafts and All Mail after editing it locally multiple times
|
||||||
|
|||||||
@ -651,3 +651,83 @@ Feature: IMAP import messages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
Scenario: Message import multipart/related with invalid boundary character
|
||||||
|
When IMAP client "1" appends the following message to "INBOX":
|
||||||
|
"""
|
||||||
|
From: Bridge Test <bridgetest@pm.test>
|
||||||
|
Date: 01 Jan 1980 00:00:00 +0000
|
||||||
|
To: Internal Bridge <bridgetest@example.com>
|
||||||
|
Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000
|
||||||
|
Subject: Message with invalid boundary
|
||||||
|
Content-Type: multipart/related; boundary="------------123456789@tutanota"
|
||||||
|
|
||||||
|
--------------123456789@tutanota
|
||||||
|
Content-Type: text/html; charset=UTF-8
|
||||||
|
Content-transfer-encoding: base64
|
||||||
|
|
||||||
|
PGRpdiBjbGFzcz0iIj4KPHAgY2xhc3M9IiI+PGEgbmFtZT0iX0hsazE5MDA1NjM2IiByZWw9Im5vb3
|
||||||
|
BlbmVyIG5vcmVmZXJyZXIiIHRhcmdldD0iX2JsYW5rIj48c3BhbiBzdHlsZT0ibXNvLWZhcmVhc3Qt
|
||||||
|
|
||||||
|
--------------123456789@tutanota
|
||||||
|
Content-Type: image/png;
|
||||||
|
name==?UTF-8?B?MC5wbmc=?=
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-Disposition: attachment;
|
||||||
|
filename=image1.png
|
||||||
|
|
||||||
|
iVBORw0KGgoAAAANSUhEUgAAACsAAAArCAYAAADhXXHAAAAPq3pUWHRSYXcgcHJvZmlsZSB0eXBlIG
|
||||||
|
V4aWYAAHjarZlrliOpkoT/s4pZAuCAw3J4njM7mOXP54SUlZmV1bd7plNVEVIoAhx/mJsht//nv4/7
|
||||||
|
|
||||||
|
--------------123456789@tutanota
|
||||||
|
Content-Type: image/png;
|
||||||
|
name==?UTF-8?B?Mi5wbmc=?=
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-Disposition: attachment;
|
||||||
|
filename=img2.png
|
||||||
|
|
||||||
|
iVBORw0KGgoAAAANSUhEUgAAACsAAAArCAYAAADhXXHAAAAR+HpUWHRSYXcgcHJvZmlsZSB0eXBlIG
|
||||||
|
V4aWYAAHjarZprdhs5DoX/cxWzBD4Bcjl8njM7mOXPB5bsOI49SU+3nViKLFWxgIv7YMXt//z7uH/x
|
||||||
|
|
||||||
|
--------------123456789@tutanota--
|
||||||
|
|
||||||
|
"""
|
||||||
|
Then it succeeds
|
||||||
|
And IMAP client "1" eventually sees the following message in "INBOX" with this structure:
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"from": "Bridge Test <bridgetest@pm.test>",
|
||||||
|
"date": "01 Jan 80 00:00 +0000",
|
||||||
|
"to": "Internal Bridge <bridgetest@example.com>",
|
||||||
|
"subject": "Message with invalid boundary",
|
||||||
|
"content": {
|
||||||
|
"content-type": "multipart/mixed",
|
||||||
|
"sections":[
|
||||||
|
{
|
||||||
|
"content-type": "multipart/related",
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"content-type": "text/html",
|
||||||
|
"transfer-encoding": "base64",
|
||||||
|
"body-is": "PGRpdiBjbGFzcz0iIj4KPHAgY2xhc3M9IiI+PGEgbmFtZT0iX0hsazE5MDA1NjM2IiByZWw9Im5v\r\nb3BlbmVyIG5vcmVmZXJyZXIiIHRhcmdldD0iX2JsYW5rIj48c3BhbiBzdHlsZT0ibXNvLWZhcmVh\r\nc3Qt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content-type": "image/png",
|
||||||
|
"transfer-encoding": "base64",
|
||||||
|
"content-disposition": "attachment",
|
||||||
|
"content-disposition-filename": "image1.png",
|
||||||
|
"body-is": "iVBORw0KGgoAAAANSUhEUgAAACsAAAArCAYAAADhXXHAAAAPq3pUWHRSYXcgcHJvZmlsZSB0eXBl\r\nIGV4aWYAAHjarZlrliOpkoT/s4pZAuCAw3J4njM7mOXP54SUlZmV1bd7plNVEVIoAhx/mJsht//n\r\nv4/7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content-type": "image/png",
|
||||||
|
"transfer-encoding": "base64",
|
||||||
|
"content-disposition": "attachment",
|
||||||
|
"content-disposition-filename": "img2.png",
|
||||||
|
"body-is": "iVBORw0KGgoAAAANSUhEUgAAACsAAAArCAYAAADhXXHAAAAR+HpUWHRSYXcgcHJvZmlsZSB0eXBl\r\nIGV4aWYAAHjarZprdhs5DoX/cxWzBD4Bcjl8njM7mOXPB5bsOI49SU+3nViKLFWxgIv7YMXt//z7\r\nuH/x"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|||||||
38
tests/features/observability/all_metrics.feature
Normal file
38
tests/features/observability/all_metrics.feature
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
Feature: Bridge send remote notification observability metrics
|
||||||
|
Background:
|
||||||
|
Given there exists an account with username "[user:user1]" and password "password"
|
||||||
|
Then it succeeds
|
||||||
|
When bridge starts
|
||||||
|
Then it succeeds
|
||||||
|
|
||||||
|
Scenario: Test all possible heartbeat metrics
|
||||||
|
When the user logs in with username "[user:user1]" and password "password"
|
||||||
|
And the user with username "[user:user1]" sends all possible observability heartbeat metrics
|
||||||
|
Then it succeeds
|
||||||
|
|
||||||
|
Scenario: Test all possible user discrimination metrics
|
||||||
|
When the user logs in with username "[user:user1]" and password "password"
|
||||||
|
And the user with username "[user:user1]" sends all possible user distinction metrics
|
||||||
|
Then it succeeds
|
||||||
|
|
||||||
|
Scenario: Test all possible sync message event failure observability metrics
|
||||||
|
When the user logs in with username "[user:user1]" and password "password"
|
||||||
|
And the user with username "[user:user1]" sends all possible sync message event failure observability metrics
|
||||||
|
Then it succeeds
|
||||||
|
|
||||||
|
Scenario: Test all possible event loop message events observability metrics
|
||||||
|
When the user logs in with username "[user:user1]" and password "password"
|
||||||
|
And the user with username "[user:user1]" sends all possible event loop message events observability metrics
|
||||||
|
Then it succeeds
|
||||||
|
|
||||||
|
Scenario: Test all possible sync message building failure observability metrics
|
||||||
|
When the user logs in with username "[user:user1]" and password "password"
|
||||||
|
And the user with username "[user:user1]" sends all possible sync message building failure observability metrics
|
||||||
|
Then it succeeds
|
||||||
|
|
||||||
|
Scenario: Test all possible sync message building success observability metrics
|
||||||
|
When the user logs in with username "[user:user1]" and password "password"
|
||||||
|
And the user with username "[user:user1]" sends all possible sync message building success observability metrics
|
||||||
|
Then it succeeds
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
111
tests/observability_test.go
Normal file
111
tests/observability_test.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
// Copyright (c) 2024 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-proton-api"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/evtloopmsgevents"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/syncmsgevents"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
|
||||||
|
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
// userHeartbeatPermutationsObservability - corresponds to bridge_generic_user_heartbeat_total_v1.schema.json.
|
||||||
|
func (s *scenario) userHeartbeatPermutationsObservability(username string) error {
|
||||||
|
metrics := observability.GenerateAllHeartbeatMetricPermutations()
|
||||||
|
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
||||||
|
batch := proton.ObservabilityBatch{Metrics: metrics}
|
||||||
|
return c.SendObservabilityBatch(ctx, batch)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// userDistinctionMetricsPermutationsObservability - corresponds to:
|
||||||
|
// bridge_sync_errors_users_total_v1.schema.json
|
||||||
|
// bridge_event_loop_events_errors_users_total_v1.schema.json.
|
||||||
|
func (s *scenario) userDistinctionMetricsPermutationsObservability(username string) error {
|
||||||
|
batch := proton.ObservabilityBatch{
|
||||||
|
Metrics: observability.GenerateAllUsedDistinctionMetricPermutations()}
|
||||||
|
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
||||||
|
err := c.SendObservabilityBatch(ctx, batch)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncFailureMessageEventsObservability - corresponds to bridge_sync_message_event_failures_total_v1.schema.json.
|
||||||
|
func (s *scenario) syncFailureMessageEventsObservability(username string) error {
|
||||||
|
batch := proton.ObservabilityBatch{
|
||||||
|
Metrics: []proton.ObservabilityMetric{
|
||||||
|
syncmsgevents.GenerateSyncFailureCreateMessageEventMetric(),
|
||||||
|
syncmsgevents.GenerateSyncFailureDeleteMessageEventMetric(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
||||||
|
err := c.SendObservabilityBatch(ctx, batch)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// eventLoopFailureMessageEventsObservability - corresponds to bridge_event_loop_message_event_failures_total_v1.schema.json.
|
||||||
|
func (s *scenario) eventLoopFailureMessageEventsObservability(username string) error {
|
||||||
|
batch := proton.ObservabilityBatch{
|
||||||
|
Metrics: []proton.ObservabilityMetric{
|
||||||
|
evtloopmsgevents.GenerateMessageEventFailedToBuildDraft(),
|
||||||
|
evtloopmsgevents.GenerateMessageEventFailedToBuildMessage(),
|
||||||
|
evtloopmsgevents.GenerateMessageEventFailureCreateMessageMetric(),
|
||||||
|
evtloopmsgevents.GenerateMessageEventFailureDeleteMessageMetric(),
|
||||||
|
evtloopmsgevents.GenerateMessageEventFailureUpdateMetric(),
|
||||||
|
evtloopmsgevents.GenerateMessageEventUpdateChannelDoesNotExist(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
||||||
|
err := c.SendObservabilityBatch(ctx, batch)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncFailureMessageBuiltObservability - corresponds to bridge_sync_message_event_failures_total_v1.schema.json.
|
||||||
|
func (s *scenario) syncFailureMessageBuiltObservability(username string) error {
|
||||||
|
batch := proton.ObservabilityBatch{
|
||||||
|
Metrics: []proton.ObservabilityMetric{
|
||||||
|
observabilitymetrics.GenerateNoUnlockedKeyringMetric(),
|
||||||
|
observabilitymetrics.GenerateFailedToBuildMetric(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
||||||
|
err := c.SendObservabilityBatch(ctx, batch)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncSuccessMessageBuiltObservability - corresponds to bridge_sync_message_build_success_total_v1.schema.json.
|
||||||
|
func (s *scenario) syncSuccessMessageBuiltObservability(username string) error {
|
||||||
|
batch := proton.ObservabilityBatch{
|
||||||
|
Metrics: []proton.ObservabilityMetric{
|
||||||
|
observabilitymetrics.GenerateMessageBuiltSuccessMetric(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
|
||||||
|
err := c.SendObservabilityBatch(ctx, batch)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -218,6 +218,12 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) {
|
|||||||
ctx.Step(`^the contact "([^"]*)" of user "([^"]*)" has public key from file "([^"]*)"$`, s.contactOfUserHasPubKeyFromFile)
|
ctx.Step(`^the contact "([^"]*)" of user "([^"]*)" has public key from file "([^"]*)"$`, s.contactOfUserHasPubKeyFromFile)
|
||||||
|
|
||||||
// ==== OBSERVABILITY METRICS ====
|
// ==== OBSERVABILITY METRICS ====
|
||||||
ctx.Step(`^the user with username "([^"]*)" sends the following remote notification observability metric "([^"]*)"`,
|
ctx.Step(`^the user with username "([^"]*)" sends the following remote notification observability metric "([^"]*)"$`,
|
||||||
s.userRemoteNotificationMetricTest)
|
s.userRemoteNotificationMetricTest)
|
||||||
|
ctx.Step(`^the user with username "([^"]*)" sends all possible observability heartbeat metrics$`, s.userHeartbeatPermutationsObservability)
|
||||||
|
ctx.Step(`^the user with username "([^"]*)" sends all possible user distinction metrics$`, s.userDistinctionMetricsPermutationsObservability)
|
||||||
|
ctx.Step(`^the user with username "([^"]*)" sends all possible sync message event failure observability metrics$`, s.syncFailureMessageEventsObservability)
|
||||||
|
ctx.Step(`^the user with username "([^"]*)" sends all possible event loop message events observability metrics$`, s.eventLoopFailureMessageEventsObservability)
|
||||||
|
ctx.Step(`^the user with username "([^"]*)" sends all possible sync message building failure observability metrics$`, s.syncFailureMessageBuiltObservability)
|
||||||
|
ctx.Step(`^the user with username "([^"]*)" sends all possible sync message building success observability metrics$`, s.syncSuccessMessageBuiltObservability)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user