diff --git a/Makefile b/Makefile index 50b250d5..5ea783fd 100644 --- a/Makefile +++ b/Makefile @@ -256,6 +256,7 @@ mocks: mockgen --package mocks github.com/ProtonMail/gluon/async PanicHandler > internal/bridge/mocks/async_mocks.go mockgen --package mocks github.com/ProtonMail/gluon/reporter Reporter > internal/bridge/mocks/gluon_mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/updater Downloader,Installer > internal/updater/mocks/mocks.go + mockgen --package mocks github.com/ProtonMail/proton-bridge/v3/internal/telemetry HeartbeatManager > internal/telemetry/mocks/mocks.go lint: gofiles lint-golang lint-license lint-dependencies lint-changelog diff --git a/internal/telemetry/heartbeat.go b/internal/telemetry/heartbeat.go index 505621c8..6b6c7458 100644 --- a/internal/telemetry/heartbeat.go +++ b/internal/telemetry/heartbeat.go @@ -29,7 +29,7 @@ func NewHeartbeat(manager HeartbeatManager, imapPort, smtpPort int, cacheDir, ke log: logrus.WithField("pkg", "telemetry"), manager: manager, metrics: HeartbeatData{ - MeasurementGroup: "bridge.amy.usage", + MeasurementGroup: "bridge.any.usage", Event: "bridge_heartbeat", }, defaultIMAPPort: imapPort, @@ -41,7 +41,7 @@ func NewHeartbeat(manager HeartbeatManager, imapPort, smtpPort int, cacheDir, ke } func (heartbeat *Heartbeat) SetRollout(val float64) { - heartbeat.metrics.Values.Rollout = int(val * 100) + heartbeat.metrics.Dimensions.Rollout = int(val * 100) } func (heartbeat *Heartbeat) SetNbAccount(val int) { @@ -129,7 +129,7 @@ func (heartbeat *Heartbeat) SetSMTPPort(val int) { } func (heartbeat *Heartbeat) SetCacheLocation(val string) { - if val != heartbeat.defaultCache { + if val == heartbeat.defaultCache { heartbeat.metrics.Dimensions.CacheLocation = dimensionDefault } else { heartbeat.metrics.Dimensions.CacheLocation = dimensionCustom @@ -137,7 +137,7 @@ func (heartbeat *Heartbeat) SetCacheLocation(val string) { } func (heartbeat *Heartbeat) SetKeyChainPref(val string) { - if val != heartbeat.defaultKeychain { + if val == heartbeat.defaultKeychain { heartbeat.metrics.Dimensions.KeychainPref = dimensionDefault } else { heartbeat.metrics.Dimensions.KeychainPref = dimensionCustom @@ -152,7 +152,7 @@ func (heartbeat *Heartbeat) StartSending() { if heartbeat.manager.IsTelemetryAvailable() { lastSent := heartbeat.manager.GetLastHeartbeatSent() now := time.Now() - if now.Year() >= lastSent.Year() && now.YearDay() > lastSent.YearDay() { + if now.Year() > lastSent.Year() || (now.Year() == lastSent.Year() && now.YearDay() > lastSent.YearDay()) { if !heartbeat.manager.SendHeartbeat(&heartbeat.metrics) { heartbeat.log.WithFields(logrus.Fields{ "metrics": heartbeat.metrics, diff --git a/internal/telemetry/heartbeat_test.go b/internal/telemetry/heartbeat_test.go new file mode 100644 index 00000000..b08931c0 --- /dev/null +++ b/internal/telemetry/heartbeat_test.go @@ -0,0 +1,97 @@ +// 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_test + +import ( + "testing" + "time" + + "github.com/ProtonMail/proton-bridge/v3/internal/telemetry" + "github.com/ProtonMail/proton-bridge/v3/internal/telemetry/mocks" + "github.com/golang/mock/gomock" +) + +func TestHeartbeat_default_heartbeat(t *testing.T) { + withHeartbeat(t, 1143, 1025, "/tmp", "defaultKeychain", func(hb *telemetry.Heartbeat, mock *mocks.MockHeartbeatManager) { + data := telemetry.HeartbeatData{ + MeasurementGroup: "bridge.any.usage", + Event: "bridge_heartbeat", + Values: telemetry.HeartbeatValues{ + NbAccount: 1, + }, + Dimensions: telemetry.HeartbeatDimensions{ + AutoUpdate: "on", + AutoStart: "on", + Beta: "off", + Doh: "off", + SplitMode: "off", + ShowAllMail: "off", + IMAPConnectionMode: "ssl", + SMTPConnectionMode: "ssl", + IMAPPort: "default", + SMTPPort: "default", + CacheLocation: "default", + KeychainPref: "default", + PrevVersion: "1.2.3", + Rollout: 10, + }, + } + + mock.EXPECT().IsTelemetryAvailable().Return(true) + mock.EXPECT().GetLastHeartbeatSent().Return(time.Date(2022, 6, 4, 0, 0, 0, 0, time.UTC)) + mock.EXPECT().SendHeartbeat(&data).Return(true) + mock.EXPECT().SetLastHeartbeatSent(gomock.Any()).Return(nil) + + hb.StartSending() + }) +} + +func TestHeartbeat_already_sent_heartbeat(t *testing.T) { + withHeartbeat(t, 1143, 1025, "/tmp", "defaultKeychain", func(hb *telemetry.Heartbeat, mock *mocks.MockHeartbeatManager) { + mock.EXPECT().IsTelemetryAvailable().Return(true) + mock.EXPECT().GetLastHeartbeatSent().Return(time.Now().Truncate(24 * time.Hour)) + + hb.StartSending() + }) +} + +func withHeartbeat(t *testing.T, imap, smtp int, cache, keychain string, tests func(hb *telemetry.Heartbeat, mock *mocks.MockHeartbeatManager)) { + ctl := gomock.NewController(t) + defer ctl.Finish() + + manager := mocks.NewMockHeartbeatManager(ctl) + heartbeat := telemetry.NewHeartbeat(manager, imap, smtp, cache, keychain) + + heartbeat.SetRollout(0.1) + heartbeat.SetNbAccount(1) + heartbeat.SetSplitMode(false) + heartbeat.SetAutoStart(true) + heartbeat.SetAutoUpdate(true) + heartbeat.SetBeta("stable") + heartbeat.SetDoh(false) + heartbeat.SetShowAllMail(false) + heartbeat.SetIMAPConnectionMode(true) + heartbeat.SetSMTPConnectionMode(true) + heartbeat.SetIMAPPort(1143) + heartbeat.SetSMTPPort(1025) + heartbeat.SetCacheLocation("/tmp") + heartbeat.SetKeyChainPref("defaultKeychain") + heartbeat.SetPrevVersion("1.2.3") + + tests(&heartbeat, manager) +} diff --git a/internal/telemetry/mocks/mocks.go b/internal/telemetry/mocks/mocks.go new file mode 100644 index 00000000..be5251e2 --- /dev/null +++ b/internal/telemetry/mocks/mocks.go @@ -0,0 +1,92 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ProtonMail/proton-bridge/v3/internal/telemetry (interfaces: HeartbeatManager) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + time "time" + + telemetry "github.com/ProtonMail/proton-bridge/v3/internal/telemetry" + gomock "github.com/golang/mock/gomock" +) + +// MockHeartbeatManager is a mock of HeartbeatManager interface. +type MockHeartbeatManager struct { + ctrl *gomock.Controller + recorder *MockHeartbeatManagerMockRecorder +} + +// MockHeartbeatManagerMockRecorder is the mock recorder for MockHeartbeatManager. +type MockHeartbeatManagerMockRecorder struct { + mock *MockHeartbeatManager +} + +// NewMockHeartbeatManager creates a new mock instance. +func NewMockHeartbeatManager(ctrl *gomock.Controller) *MockHeartbeatManager { + mock := &MockHeartbeatManager{ctrl: ctrl} + mock.recorder = &MockHeartbeatManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHeartbeatManager) EXPECT() *MockHeartbeatManagerMockRecorder { + return m.recorder +} + +// GetLastHeartbeatSent mocks base method. +func (m *MockHeartbeatManager) GetLastHeartbeatSent() time.Time { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLastHeartbeatSent") + ret0, _ := ret[0].(time.Time) + return ret0 +} + +// GetLastHeartbeatSent indicates an expected call of GetLastHeartbeatSent. +func (mr *MockHeartbeatManagerMockRecorder) GetLastHeartbeatSent() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastHeartbeatSent", reflect.TypeOf((*MockHeartbeatManager)(nil).GetLastHeartbeatSent)) +} + +// IsTelemetryAvailable mocks base method. +func (m *MockHeartbeatManager) IsTelemetryAvailable() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsTelemetryAvailable") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsTelemetryAvailable indicates an expected call of IsTelemetryAvailable. +func (mr *MockHeartbeatManagerMockRecorder) IsTelemetryAvailable() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsTelemetryAvailable", reflect.TypeOf((*MockHeartbeatManager)(nil).IsTelemetryAvailable)) +} + +// SendHeartbeat mocks base method. +func (m *MockHeartbeatManager) SendHeartbeat(arg0 *telemetry.HeartbeatData) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendHeartbeat", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// SendHeartbeat indicates an expected call of SendHeartbeat. +func (mr *MockHeartbeatManagerMockRecorder) SendHeartbeat(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeartbeat", reflect.TypeOf((*MockHeartbeatManager)(nil).SendHeartbeat), arg0) +} + +// SetLastHeartbeatSent mocks base method. +func (m *MockHeartbeatManager) SetLastHeartbeatSent(arg0 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetLastHeartbeatSent", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetLastHeartbeatSent indicates an expected call of SetLastHeartbeatSent. +func (mr *MockHeartbeatManagerMockRecorder) SetLastHeartbeatSent(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLastHeartbeatSent", reflect.TypeOf((*MockHeartbeatManager)(nil).SetLastHeartbeatSent), arg0) +} diff --git a/internal/telemetry/types_heartbeat.go b/internal/telemetry/types_heartbeat.go index ed9d2828..e41a9c2d 100644 --- a/internal/telemetry/types_heartbeat.go +++ b/internal/telemetry/types_heartbeat.go @@ -40,7 +40,6 @@ type HeartbeatManager interface { } type HeartbeatValues struct { - Rollout int `json:"rollout"` NbAccount int `json:"nb_account"` } @@ -58,6 +57,7 @@ type HeartbeatDimensions struct { CacheLocation string `json:"cache_location"` KeychainPref string `json:"keychain_pref"` PrevVersion string `json:"prev_version"` + Rollout int `json:"rollout"` } type HeartbeatData struct {