diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go index 9cf32662..f633cb0b 100644 --- a/internal/bridge/bridge.go +++ b/internal/bridge/bridge.go @@ -41,8 +41,10 @@ import ( "github.com/ProtonMail/proton-bridge/v3/internal/focus" "github.com/ProtonMail/proton-bridge/v3/internal/safe" "github.com/ProtonMail/proton-bridge/v3/internal/sentry" + "github.com/ProtonMail/proton-bridge/v3/internal/telemetry" "github.com/ProtonMail/proton-bridge/v3/internal/user" "github.com/ProtonMail/proton-bridge/v3/internal/vault" + "github.com/ProtonMail/proton-bridge/v3/pkg/keychain" "github.com/bradenaw/juniper/xslices" "github.com/emersion/go-smtp" "github.com/go-resty/resty/v2" @@ -78,6 +80,9 @@ type Bridge struct { updater Updater installCh chan installJob + // heartbeat is the telemetry heartbeat for metrics. + heartbeat telemetry.Heartbeat + // curVersion is the current version of the bridge, // newVersion is the version that was installed by the updater. curVersion *semver.Version @@ -278,6 +283,8 @@ func newBridge( updater: updater, installCh: make(chan installJob), + heartbeat: telemetry.NewHeartbeat(1143, 1025, gluonCacheDir, keychain.DefaultHelper), + curVersion: curVersion, newVersion: curVersion, newVersionLock: safe.NewRWMutex(), @@ -423,6 +430,8 @@ func (bridge *Bridge) init(tlsReporter TLSReporter) error { }) }) + // init telemetry + bridge.initHeartbeat() return nil } @@ -479,22 +488,6 @@ func (bridge *Bridge) Close(ctx context.Context) { bridge.watchers = nil } -func (bridge *Bridge) ComputeTelemetry() bool { - if bridge.GetTelemetryDisabled() { - return false - } - - var telemetry = true - - safe.RLock(func() { - for _, user := range bridge.users { - telemetry = telemetry && user.IsTelemetryEnabled(context.Background()) - } - }, bridge.usersLock) - - return telemetry -} - func (bridge *Bridge) publish(event events.Event) { bridge.watchersLock.RLock() defer bridge.watchersLock.RUnlock() diff --git a/internal/bridge/heartbeat.go b/internal/bridge/heartbeat.go new file mode 100644 index 00000000..c7ed4b20 --- /dev/null +++ b/internal/bridge/heartbeat.go @@ -0,0 +1,67 @@ +// Copyright (c) 2023 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 . + +package bridge + +import ( + "context" + + "github.com/ProtonMail/proton-bridge/v3/internal/safe" + "github.com/ProtonMail/proton-bridge/v3/internal/vault" +) + +func (bridge *Bridge) ComputeTelemetry() bool { + var telemetry = true + + safe.RLock(func() { + for _, user := range bridge.users { + telemetry = telemetry && user.IsTelemetryEnabled(context.Background()) + } + }, bridge.usersLock) + + return telemetry +} + +func (bridge *Bridge) initHeartbeat() { + safe.RLock(func() { + var splitMode = false + for _, user := range bridge.users { + if user.GetAddressMode() == vault.SplitMode { + splitMode = true + break + } + } + bridge.heartbeat.SetNbAccount(len(bridge.users)) + bridge.heartbeat.SetSplitMode(splitMode) + }, bridge.usersLock) + + bridge.heartbeat.SetRollout(bridge.GetUpdateRollout()) + bridge.heartbeat.SetAutoStart(bridge.GetAutostart()) + bridge.heartbeat.SetAutoUpdate(bridge.GetAutoUpdate()) + bridge.heartbeat.SetBeta(bridge.GetUpdateChannel()) + bridge.heartbeat.SetDoh(bridge.GetProxyAllowed()) + bridge.heartbeat.SetShowAllMail(bridge.GetShowAllMail()) + bridge.heartbeat.SetIMAPConnectionMode(bridge.GetIMAPSSL()) + bridge.heartbeat.SetSMTPConnectionMode(bridge.GetSMTPSSL()) + bridge.heartbeat.SetIMAPPort(bridge.GetIMAPPort()) + bridge.heartbeat.SetSMTPPort(bridge.GetSMTPPort()) + bridge.heartbeat.SetCacheLocation(bridge.GetGluonCacheDir()) + if val, err := bridge.GetKeychainApp(); err != nil { + bridge.heartbeat.SetKeyChainPref(val) + } + bridge.heartbeat.SetPrevVersion(bridge.GetLastVersion().String()) +} diff --git a/internal/bridge/settings.go b/internal/bridge/settings.go index 1b38f323..0da85403 100644 --- a/internal/bridge/settings.go +++ b/internal/bridge/settings.go @@ -46,6 +46,8 @@ func (bridge *Bridge) SetKeychainApp(helper string) error { return err } + bridge.heartbeat.SetKeyChainPref(helper) + return vault.SetHelper(vaultDir, helper) } @@ -62,6 +64,8 @@ func (bridge *Bridge) SetIMAPPort(newPort int) error { return err } + bridge.heartbeat.SetIMAPPort(newPort) + return bridge.restartIMAP() } @@ -78,6 +82,8 @@ func (bridge *Bridge) SetIMAPSSL(newSSL bool) error { return err } + bridge.heartbeat.SetIMAPConnectionMode(newSSL) + return bridge.restartIMAP() } @@ -94,6 +100,8 @@ func (bridge *Bridge) SetSMTPPort(newPort int) error { return err } + bridge.heartbeat.SetSMTPPort(newPort) + return bridge.restartSMTP() } @@ -110,6 +118,8 @@ func (bridge *Bridge) SetSMTPSSL(newSSL bool) error { return err } + bridge.heartbeat.SetSMTPConnectionMode(newSSL) + return bridge.restartSMTP() } @@ -141,6 +151,8 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error } } + bridge.heartbeat.SetCacheLocation(newGluonDir) + gluonDataDir, err := bridge.GetGluonDataDir() if err != nil { return fmt.Errorf("failed to get Gluon Database directory: %w", err) @@ -207,6 +219,8 @@ func (bridge *Bridge) SetProxyAllowed(allowed bool) error { bridge.proxyCtl.DisallowProxy() } + bridge.heartbeat.SetDoh(allowed) + return bridge.vault.SetProxyAllowed(allowed) } @@ -220,6 +234,8 @@ func (bridge *Bridge) SetShowAllMail(show bool) error { user.SetShowAllMail(show) } + bridge.heartbeat.SetShowAllMail(show) + return bridge.vault.SetShowAllMail(show) }, bridge.usersLock) } @@ -233,6 +249,8 @@ func (bridge *Bridge) SetAutostart(autostart bool) error { if err := bridge.vault.SetAutostart(autostart); err != nil { return err } + + bridge.heartbeat.SetAutoStart(autostart) } var err error @@ -253,6 +271,10 @@ func (bridge *Bridge) SetAutostart(autostart bool) error { return err } +func (bridge *Bridge) GetUpdateRollout() float64 { + return bridge.vault.GetUpdateRollout() +} + func (bridge *Bridge) GetAutoUpdate() bool { return bridge.vault.GetAutoUpdate() } @@ -266,6 +288,8 @@ func (bridge *Bridge) SetAutoUpdate(autoUpdate bool) error { return err } + bridge.heartbeat.SetAutoUpdate(autoUpdate) + bridge.goUpdate() return nil @@ -292,6 +316,8 @@ func (bridge *Bridge) SetUpdateChannel(channel updater.Channel) error { return err } + bridge.heartbeat.SetBeta(channel) + bridge.goUpdate() return nil diff --git a/internal/bridge/user.go b/internal/bridge/user.go index 907b8594..4bb2a455 100644 --- a/internal/bridge/user.go +++ b/internal/bridge/user.go @@ -295,6 +295,15 @@ func (bridge *Bridge) SetAddressMode(ctx context.Context, userID string, mode va AddressMode: mode, }) + var splitMode = false + for _, user := range bridge.users { + if user.GetAddressMode() == vault.SplitMode { + splitMode = true + break + } + } + bridge.heartbeat.SetSplitMode(splitMode) + return nil }, bridge.usersLock) } @@ -559,6 +568,7 @@ func (bridge *Bridge) addUserWithVault( // Finally, save the user in the bridge. safe.Lock(func() { bridge.users[apiUser.ID] = user + bridge.heartbeat.SetNbAccount(len(bridge.users)) }, bridge.usersLock) return nil @@ -614,6 +624,8 @@ func (bridge *Bridge) logoutUser(ctx context.Context, user *user.User, withAPI, logrus.WithError(err).Error("Failed to logout user") } + bridge.heartbeat.SetNbAccount(len(bridge.users)) + user.Close() } diff --git a/internal/telemetry/heartbeat.go b/internal/telemetry/heartbeat.go new file mode 100644 index 00000000..5ee58fd2 --- /dev/null +++ b/internal/telemetry/heartbeat.go @@ -0,0 +1,144 @@ +// Copyright (c) 2023 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 . + +package telemetry + +import ( + "github.com/ProtonMail/proton-bridge/v3/internal/updater" +) + +func NewHeartbeat(imapPort, smtpPort int, cacheDir, keychain string) Heartbeat { + heartbeat := Heartbeat{ + Metrics: HeartbeatData{ + MeasurementGroup: "bridge.amy.usage", + Event: "bridge_heartbeat", + }, + DefaultIMAPPort: imapPort, + DefaultSMTPPort: smtpPort, + DefaultCache: cacheDir, + DefaultKeychain: keychain, + } + return heartbeat +} + +func (heartbeat *Heartbeat) SetRollout(val float64) { + heartbeat.Metrics.Values.Rollout = int(val * 100) +} + +func (heartbeat *Heartbeat) SetNbAccount(val int) { + heartbeat.Metrics.Values.NbAccount = val +} + +func (heartbeat *Heartbeat) SetAutoUpdate(val bool) { + if val { + heartbeat.Metrics.Dimensions.AutoUpdate = dimensionON + } else { + heartbeat.Metrics.Dimensions.AutoUpdate = dimensionOFF + } +} + +func (heartbeat *Heartbeat) SetAutoStart(val bool) { + if val { + heartbeat.Metrics.Dimensions.AutoStart = dimensionON + } else { + heartbeat.Metrics.Dimensions.AutoStart = dimensionOFF + } +} + +func (heartbeat *Heartbeat) SetBeta(val updater.Channel) { + if val == updater.EarlyChannel { + heartbeat.Metrics.Dimensions.Beta = dimensionON + } else { + heartbeat.Metrics.Dimensions.Beta = dimensionOFF + } +} + +func (heartbeat *Heartbeat) SetDoh(val bool) { + if val { + heartbeat.Metrics.Dimensions.Doh = dimensionON + } else { + heartbeat.Metrics.Dimensions.Doh = dimensionOFF + } +} + +func (heartbeat *Heartbeat) SetSplitMode(val bool) { + if val { + heartbeat.Metrics.Dimensions.SplitMode = dimensionON + } else { + heartbeat.Metrics.Dimensions.SplitMode = dimensionOFF + } +} + +func (heartbeat *Heartbeat) SetShowAllMail(val bool) { + if val { + heartbeat.Metrics.Dimensions.ShowAllMail = dimensionON + } else { + heartbeat.Metrics.Dimensions.ShowAllMail = dimensionOFF + } +} + +func (heartbeat *Heartbeat) SetIMAPConnectionMode(val bool) { + if val { + heartbeat.Metrics.Dimensions.IMAPConnectionMode = dimensionSSL + } else { + heartbeat.Metrics.Dimensions.IMAPConnectionMode = dimensionStartTLS + } +} + +func (heartbeat *Heartbeat) SetSMTPConnectionMode(val bool) { + if val { + heartbeat.Metrics.Dimensions.SMTPConnectionMode = dimensionSSL + } else { + heartbeat.Metrics.Dimensions.SMTPConnectionMode = dimensionStartTLS + } +} + +func (heartbeat *Heartbeat) SetIMAPPort(val int) { + if val == heartbeat.DefaultIMAPPort { + heartbeat.Metrics.Dimensions.IMAPPort = dimensionDefault + } else { + heartbeat.Metrics.Dimensions.IMAPPort = dimensionCustom + } +} + +func (heartbeat *Heartbeat) SetSMTPPort(val int) { + if val == heartbeat.DefaultSMTPPort { + heartbeat.Metrics.Dimensions.SMTPPort = dimensionDefault + } else { + heartbeat.Metrics.Dimensions.SMTPPort = dimensionCustom + } +} + +func (heartbeat *Heartbeat) SetCacheLocation(val string) { + if val != heartbeat.DefaultCache { + heartbeat.Metrics.Dimensions.CacheLocation = dimensionDefault + } else { + heartbeat.Metrics.Dimensions.CacheLocation = dimensionCustom + } +} + +func (heartbeat *Heartbeat) SetKeyChainPref(val string) { + if val != heartbeat.DefaultKeychain { + heartbeat.Metrics.Dimensions.KeychainPref = dimensionDefault + } else { + heartbeat.Metrics.Dimensions.KeychainPref = dimensionCustom + } +} + +func (heartbeat *Heartbeat) SetPrevVersion(val string) { + heartbeat.Metrics.Dimensions.PrevVersion = val +} diff --git a/internal/telemetry/heartbeat_type.go b/internal/telemetry/heartbeat_type.go new file mode 100644 index 00000000..604c5a38 --- /dev/null +++ b/internal/telemetry/heartbeat_type.go @@ -0,0 +1,64 @@ +// Copyright (c) 2023 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 . + +package telemetry + +const ( + dimensionON = "on" + dimensionOFF = "off" + dimensionDefault = "default" + dimensionCustom = "custom" + dimensionSSL = "ssl" + dimensionStartTLS = "starttls" +) + +type HeartbeatValues struct { + Rollout int `json:"rollout"` + NbAccount int `json:"nb_account"` +} + +type HeartbeatDimensions struct { + AutoUpdate string `json:"auto_update"` + AutoStart string `json:"auto_start"` + Beta string `json:"beta"` + Doh string `json:"doh"` + SplitMode string `json:"split_mode"` + ShowAllMail string `json:"show_all_mail"` + IMAPConnectionMode string `json:"imap_connection_mode"` + SMTPConnectionMode string `json:"smtp_connection_mode"` + IMAPPort string `json:"imap_port"` + SMTPPort string `json:"smtp_port"` + CacheLocation string `json:"cache_location"` + KeychainPref string `json:"keychain_pref"` + PrevVersion string `json:"prev_version"` +} + +type HeartbeatData struct { + MeasurementGroup string + Event string + Values HeartbeatValues + Dimensions HeartbeatDimensions +} + +type Heartbeat struct { + Metrics HeartbeatData + + DefaultIMAPPort int + DefaultSMTPPort int + DefaultCache string + DefaultKeychain string +} diff --git a/pkg/keychain/helper_darwin.go b/pkg/keychain/helper_darwin.go index 2e36c9d2..9faae613 100644 --- a/pkg/keychain/helper_darwin.go +++ b/pkg/keychain/helper_darwin.go @@ -38,7 +38,7 @@ func init() { //nolint:gochecknoinits Helpers[MacOSKeychain] = newMacOSHelper // Use MacOSKeychain by default. - defaultHelper = MacOSKeychain + DefaultHelper = MacOSKeychain } func parseError(original error) error { diff --git a/pkg/keychain/helper_linux.go b/pkg/keychain/helper_linux.go index 3c31736d..ea146fc7 100644 --- a/pkg/keychain/helper_linux.go +++ b/pkg/keychain/helper_linux.go @@ -48,14 +48,14 @@ func init() { //nolint:gochecknoinits Helpers[Pass] = newPassHelper } - defaultHelper = SecretServiceDBus + DefaultHelper = SecretServiceDBus // If Pass is available, use it by default. // Otherwise, if SecretService is available, use it by default. if _, ok := Helpers[Pass]; ok { - defaultHelper = Pass + DefaultHelper = Pass } else if _, ok := Helpers[SecretService]; ok { - defaultHelper = SecretService + DefaultHelper = SecretService } } diff --git a/pkg/keychain/helper_windows.go b/pkg/keychain/helper_windows.go index 6eb2dd06..b4b8ccbd 100644 --- a/pkg/keychain/helper_windows.go +++ b/pkg/keychain/helper_windows.go @@ -31,7 +31,7 @@ func init() { //nolint:gochecknoinits Helpers[WindowsCredentials] = newWinCredHelper // Use WindowsCredentials by default. - defaultHelper = WindowsCredentials + DefaultHelper = WindowsCredentials } func newWinCredHelper(string) (credentials.Helper, error) { diff --git a/pkg/keychain/keychain.go b/pkg/keychain/keychain.go index 251477c9..57b895c9 100644 --- a/pkg/keychain/keychain.go +++ b/pkg/keychain/keychain.go @@ -42,8 +42,8 @@ var ( // Helpers holds all discovered keychain helpers. It is populated in init(). Helpers map[string]helperConstructor //nolint:gochecknoglobals - // defaultHelper is the default helper to use if the user hasn't yet set a preference. - defaultHelper string //nolint:gochecknoglobals + // DefaultHelper is the default helper to use if the user hasn't yet set a preference. + DefaultHelper string //nolint:gochecknoglobals ) // NewKeychain creates a new native keychain. @@ -55,7 +55,7 @@ func NewKeychain(preferred, keychainName string) (*Keychain, error) { // If the preferred keychain is unsupported, fallback to the default one. if _, ok := Helpers[preferred]; !ok { - preferred = defaultHelper + preferred = DefaultHelper } // Load the user's preferred keychain helper.