Add metrics for mobile versions snapshots (#28191)

* Add metrics for mobile versions snapshots

* Add notifications disabled and fix lint

* Address feedback

* Verify all references to JobTypeActiveUsers

* Fix typos

* Improve platform values

* Add test and MySQL support
Этот коммит содержится в:
Daniel Espino García
2024-09-24 12:02:19 +02:00
коммит произвёл GitHub
родитель d45a54a8e9
Коммит 040838b056
17 изменённых файлов: 489 добавлений и 0 удалений

Просмотреть файл

@@ -175,6 +175,7 @@ func (a *App) SessionHasPermissionToReadJob(session model.Session, jobType strin
model.JobTypeExportProcess,
model.JobTypeExportDelete,
model.JobTypeCloud,
model.JobTypeMobileSessionMetadata,
model.JobTypeExtractContent:
return a.SessionHasPermissionTo(session, model.PermissionReadJobs), model.PermissionReadJobs
}

Просмотреть файл

@@ -55,6 +55,7 @@ import (
"github.com/mattermost/mattermost/server/v8/channels/jobs/last_accessible_file"
"github.com/mattermost/mattermost/server/v8/channels/jobs/last_accessible_post"
"github.com/mattermost/mattermost/server/v8/channels/jobs/migrations"
"github.com/mattermost/mattermost/server/v8/channels/jobs/mobile_session_metadata"
"github.com/mattermost/mattermost/server/v8/channels/jobs/notify_admin"
"github.com/mattermost/mattermost/server/v8/channels/jobs/plugins"
"github.com/mattermost/mattermost/server/v8/channels/jobs/post_persistent_notifications"
@@ -1565,6 +1566,12 @@ func (s *Server) initJobs() {
active_users.MakeScheduler(s.Jobs),
)
s.Jobs.RegisterJobType(
model.JobTypeMobileSessionMetadata,
mobile_session_metadata.MakeWorker(s.Jobs, s.Store(), func() einterfaces.MetricsInterface { return s.GetMetrics() }),
mobile_session_metadata.MakeScheduler(s.Jobs),
)
s.Jobs.RegisterJobType(
model.JobTypeResendInvitationEmail,
resend_invitation_email.MakeWorker(s.Jobs, New(ServerConnector(s.Channels())), s.Store(), s.telemetryService),

Просмотреть файл

@@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mobile_session_metadata
import (
"time"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/jobs"
)
const schedFreq = 24 * time.Hour
func MakeScheduler(jobServer *jobs.JobServer) *jobs.PeriodicScheduler {
isEnabled := func(cfg *model.Config) bool {
return *cfg.MetricsSettings.EnableClientMetrics
}
return jobs.NewPeriodicScheduler(jobServer, model.JobTypeMobileSessionMetadata, schedFreq, isEnabled)
}

Просмотреть файл

@@ -0,0 +1,42 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package mobile_session_metadata
import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/jobs"
"github.com/mattermost/mattermost/server/v8/channels/store"
"github.com/mattermost/mattermost/server/v8/einterfaces"
)
func MakeWorker(jobServer *jobs.JobServer, store store.Store, getMetrics func() einterfaces.MetricsInterface) *jobs.SimpleWorker {
const workerName = "MobileSessionMetadata"
isEnabled := func(cfg *model.Config) bool {
return *cfg.MetricsSettings.EnableClientMetrics
}
execute := func(logger mlog.LoggerIFace, job *model.Job) error {
defer jobServer.HandleJobPanic(logger, job)
metrics := getMetrics()
if metrics == nil {
return nil
}
versions, err := store.Session().GetMobileSessionMetadata()
if err != nil {
return err
}
metrics.ClearMobileClientSessionMetadata()
for _, v := range versions {
metrics.ObserveMobileClientSessionMetadata(v.Version, v.Platform, v.Count, v.NotificationDisabled)
}
return nil
}
worker := jobs.NewSimpleWorker(workerName, jobServer, execute, isEnabled)
return worker
}

Просмотреть файл

@@ -8710,6 +8710,24 @@ func (s *OpenTracingLayerSessionStore) GetLRUSessions(c request.CTX, userID stri
return result, err
}
func (s *OpenTracingLayerSessionStore) GetMobileSessionMetadata() ([]*model.MobileSessionMetadata, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.GetMobileSessionMetadata")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.SessionStore.GetMobileSessionMetadata()
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerSessionStore) GetSessions(c request.CTX, userID string) ([]*model.Session, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.GetSessions")

Просмотреть файл

@@ -9938,6 +9938,27 @@ func (s *RetryLayerSessionStore) GetLRUSessions(c request.CTX, userID string, li
}
func (s *RetryLayerSessionStore) GetMobileSessionMetadata() ([]*model.MobileSessionMetadata, error) {
tries := 0
for {
result, err := s.SessionStore.GetMobileSessionMetadata()
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerSessionStore) GetSessions(c request.CTX, userID string) ([]*model.Session, error) {
tries := 0

Просмотреть файл

@@ -164,6 +164,38 @@ func (me SqlSessionStore) GetSessionsWithActiveDeviceIds(userId string) ([]*mode
return sessions, nil
}
func (me SqlSessionStore) GetMobileSessionMetadata() ([]*model.MobileSessionMetadata, error) {
versionProp := model.SessionPropMobileVersion
notificationDisabledProp := model.SessionPropDeviceNotificationDisabled
platformQuery := "NULLIF(SPLIT_PART(deviceid, ':', 1), '')"
if me.DriverName() == model.DatabaseDriverMysql {
versionProp = "$." + versionProp
notificationDisabledProp = "$." + notificationDisabledProp
platformQuery = "NULLIF(SUBSTRING_INDEX(deviceid, ':', 1), deviceid)"
}
query, args, err := me.getQueryBuilder().
Select(fmt.Sprintf(
"COUNT(userid) AS Count, COALESCE(%s,'N/A') AS Platform, COALESCE(props->>'%s','N/A') AS Version, COALESCE(props->>'%s','false') as NotificationDisabled",
platformQuery,
versionProp,
notificationDisabledProp,
)).
From("Sessions").
GroupBy("Platform", "Version", "NotificationDisabled").
ToSql()
if err != nil {
return nil, errors.Wrap(err, "sessions_tosql")
}
versions := []*model.MobileSessionMetadata{}
err = me.GetReplicaX().Select(&versions, query, args...)
if err != nil {
return nil, errors.Wrap(err, "failed get mobile session metadata")
}
return versions, nil
}
func (me SqlSessionStore) GetSessionsExpired(thresholdMillis int64, mobileOnly bool, unnotifiedOnly bool) ([]*model.Session, error) {
now := model.GetMillis()
builder := me.getQueryBuilder().

Просмотреть файл

@@ -503,6 +503,7 @@ type SessionStore interface {
Save(c request.CTX, session *model.Session) (*model.Session, error)
GetSessions(c request.CTX, userID string) ([]*model.Session, error)
GetLRUSessions(c request.CTX, userID string, limit uint64, offset uint64) ([]*model.Session, error)
GetMobileSessionMetadata() ([]*model.MobileSessionMetadata, error)
GetSessionsWithActiveDeviceIds(userID string) ([]*model.Session, error)
GetSessionsExpired(thresholdMillis int64, mobileOnly bool, unnotifiedOnly bool) ([]*model.Session, error)
UpdateExpiredNotify(sessionid string, notified bool) error

Просмотреть файл

@@ -121,6 +121,36 @@ func (_m *SessionStore) GetLRUSessions(c request.CTX, userID string, limit uint6
return r0, r1
}
// GetMobileSessionMetadata provides a mock function with given fields:
func (_m *SessionStore) GetMobileSessionMetadata() ([]*model.MobileSessionMetadata, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetMobileSessionMetadata")
}
var r0 []*model.MobileSessionMetadata
var r1 error
if rf, ok := ret.Get(0).(func() ([]*model.MobileSessionMetadata, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() []*model.MobileSessionMetadata); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.MobileSessionMetadata)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSessions provides a mock function with given fields: c, userID
func (_m *SessionStore) GetSessions(c request.CTX, userID string) ([]*model.Session, error) {
ret := _m.Called(c, userID)

Просмотреть файл

@@ -38,6 +38,7 @@ func TestSessionStore(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("GetSessionsExpired", func(t *testing.T) { testGetSessionsExpired(t, rctx, ss) })
t.Run("UpdateExpiredNotify", func(t *testing.T) { testUpdateExpiredNotify(t, rctx, ss) })
t.Run("GetLRUSessions", func(t *testing.T) { testGetLRUSessions(t, rctx, ss) })
t.Run("GetMobileSessionMetadata", func(t *testing.T) { testGetMobileSessionMetadata(t, rctx, ss) })
}
func testSessionStoreSave(t *testing.T, rctx request.CTX, ss store.Store) {
@@ -456,3 +457,84 @@ func testGetLRUSessions(t *testing.T, rctx request.CTX, ss store.Store) {
require.Equal(t, s2.Id, sessions[1].Id)
require.Equal(t, s1.Id, sessions[2].Id)
}
func testGetMobileSessionMetadata(t *testing.T, rctx request.CTX, ss store.Store) {
userId1 := model.NewId()
userId2 := model.NewId()
userId3 := model.NewId()
userId4 := model.NewId()
userId5 := model.NewId()
// Clear existing sessions.
err := ss.Session().RemoveAllSessions()
require.NoError(t, err)
s1 := &model.Session{}
s1.UserId = userId1
s1.ExpiresAt = model.GetMillis() + 10000
_, err = ss.Session().Save(rctx, s1)
require.NoError(t, err)
s2 := &model.Session{}
s2.UserId = userId2
s2.DeviceId = "android:" + model.NewId()
s2.ExpiresAt = model.GetMillis() + 10000
s2.Props = model.StringMap{
model.SessionPropDeviceNotificationDisabled: "false",
model.SessionPropMobileVersion: "1.2.3",
}
_, err = ss.Session().Save(rctx, s2)
require.NoError(t, err)
s3 := &model.Session{}
s3.UserId = userId3
s3.DeviceId = "ios:" + model.NewId()
s3.ExpiresAt = model.GetMillis() + 10000
s3.Props = model.StringMap{
model.SessionPropDeviceNotificationDisabled: "true",
model.SessionPropMobileVersion: "1.2.3",
}
_, err = ss.Session().Save(rctx, s3)
require.NoError(t, err)
s4 := &model.Session{}
s4.UserId = userId4
s4.DeviceId = "android:" + model.NewId()
s4.ExpiresAt = model.GetMillis() + 10000
s4.Props = model.StringMap{
model.SessionPropDeviceNotificationDisabled: "true",
model.SessionPropMobileVersion: "3.2.1",
}
_, err = ss.Session().Save(rctx, s4)
require.NoError(t, err)
s5 := &model.Session{}
s5.UserId = userId5
s5.DeviceId = "android:" + model.NewId()
s5.ExpiresAt = model.GetMillis() + 10000
s5.Props = model.StringMap{
model.SessionPropDeviceNotificationDisabled: "true",
model.SessionPropMobileVersion: "3.2.1",
}
_, err = ss.Session().Save(rctx, s5)
require.NoError(t, err)
metadata, err := ss.Session().GetMobileSessionMetadata()
require.NoError(t, err)
require.Len(t, metadata, 4)
found := false
for _, d := range metadata {
if d.NotificationDisabled == "true" &&
d.Platform == "android" &&
d.Version == "3.2.1" {
found = true
require.Equal(t, float64(2), d.Count)
}
}
require.True(t, found)
}

Просмотреть файл

@@ -7849,6 +7849,22 @@ func (s *TimerLayerSessionStore) GetLRUSessions(c request.CTX, userID string, li
return result, err
}
func (s *TimerLayerSessionStore) GetMobileSessionMetadata() ([]*model.MobileSessionMetadata, error) {
start := time.Now()
result, err := s.SessionStore.GetMobileSessionMetadata()
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.GetMobileSessionMetadata", success, elapsed)
}
return result, err
}
func (s *TimerLayerSessionStore) GetSessions(c request.CTX, userID string) ([]*model.Session, error) {
start := time.Now()