diff --git a/server/channels/api4/metrics_test.go b/server/channels/api4/metrics_test.go index 8f6e6a436b..c22560fe44 100644 --- a/server/channels/api4/metrics_test.go +++ b/server/channels/api4/metrics_test.go @@ -91,7 +91,11 @@ func TestSubmitMetrics(t *testing.T) { t.Run("metrics enabled and valid", func(t *testing.T) { metricsMock := setupMetricsMock() - metricsMock.On("IncrementClientLongTasks", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("float64")).Return() + metricsMock.On("IncrementClientLongTasks", + mock.AnythingOfType("string"), + mock.AnythingOfType("string"), + mock.AnythingOfType("string"), + mock.AnythingOfType("float64")).Return() platform.RegisterMetricsInterface(func(_ *platform.PlatformService, _, _ string) einterfaces.MetricsInterface { return metricsMock @@ -159,7 +163,11 @@ func TestSubmitMetrics(t *testing.T) { t.Run("metrics recorded for API errors", func(t *testing.T) { metricsMock := setupMetricsMock() - metricsMock.On("IncrementClientLongTasks", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("float64")).Return() + metricsMock.On("IncrementClientLongTasks", + mock.AnythingOfType("string"), + mock.AnythingOfType("string"), + mock.AnythingOfType("string"), + mock.AnythingOfType("float64")).Return() platform.RegisterMetricsInterface(func(_ *platform.PlatformService, _, _ string) einterfaces.MetricsInterface { return metricsMock @@ -190,7 +198,11 @@ func TestSubmitMetrics(t *testing.T) { t.Run("metrics recorded for URL length limit errors", func(t *testing.T) { metricsMock := setupMetricsMock() - metricsMock.On("IncrementClientLongTasks", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("float64")).Return() + metricsMock.On("IncrementClientLongTasks", + mock.AnythingOfType("string"), + mock.AnythingOfType("string"), + mock.AnythingOfType("string"), + mock.AnythingOfType("float64")).Return() platform.RegisterMetricsInterface(func(_ *platform.PlatformService, _, _ string) einterfaces.MetricsInterface { return metricsMock diff --git a/server/channels/app/metrics.go b/server/channels/app/metrics.go index eb316b29d5..7bf8d2dc50 100644 --- a/server/channels/app/metrics.go +++ b/server/channels/app/metrics.go @@ -19,7 +19,7 @@ func (a *App) RegisterPerformanceReport(rctx request.CTX, report *model.Performa for _, c := range report.Counters { switch c.Metric { case model.ClientLongTasks: - a.Metrics().IncrementClientLongTasks(commonLabels["platform"], commonLabels["agent"], c.Value) + a.Metrics().IncrementClientLongTasks(commonLabels["platform"], commonLabels["agent"], userID, c.Value) default: // we intentionally skip unknown metrics } @@ -50,22 +50,26 @@ func (a *App) RegisterPerformanceReport(rctx request.CTX, report *model.Performa case model.ClientFirstContentfulPaint: a.Metrics().ObserveClientFirstContentfulPaint(commonLabels["platform"], commonLabels["agent"], + userID, h.Value/1000) case model.ClientLargestContentfulPaint: a.Metrics().ObserveClientLargestContentfulPaint( commonLabels["platform"], commonLabels["agent"], h.GetLabelValue("region", model.AcceptedLCPRegions, "other"), + userID, h.Value/1000) case model.ClientInteractionToNextPaint: a.Metrics().ObserveClientInteractionToNextPaint( commonLabels["platform"], commonLabels["agent"], h.GetLabelValue("interaction", model.AcceptedInteractions, "other"), + userID, h.Value/1000) case model.ClientCumulativeLayoutShift: a.Metrics().ObserveClientCumulativeLayoutShift(commonLabels["platform"], commonLabels["agent"], + userID, h.Value) case model.ClientPageLoadDuration: a.Metrics().ObserveClientPageLoadDuration(commonLabels["platform"], @@ -76,20 +80,24 @@ func (a *App) RegisterPerformanceReport(rctx request.CTX, report *model.Performa commonLabels["platform"], commonLabels["agent"], h.GetLabelValue("fresh", model.AcceptedTrueFalseLabels, ""), + userID, h.Value/1000) case model.ClientTeamSwitchDuration: a.Metrics().ObserveClientTeamSwitchDuration( commonLabels["platform"], commonLabels["agent"], h.GetLabelValue("fresh", model.AcceptedTrueFalseLabels, ""), + userID, h.Value/1000) case model.ClientRHSLoadDuration: a.Metrics().ObserveClientRHSLoadDuration(commonLabels["platform"], commonLabels["agent"], + userID, h.Value/1000) case model.ClientGlobalThreadsLoadDuration: a.Metrics().ObserveGlobalThreadsLoadDuration(commonLabels["platform"], commonLabels["agent"], + userID, h.Value/1000) case model.MobileClientLoadDuration: a.Metrics().ObserveMobileClientLoadDuration(commonLabels["platform"], diff --git a/server/einterfaces/metrics.go b/server/einterfaces/metrics.go index bd2930899a..62ee54ddbe 100644 --- a/server/einterfaces/metrics.go +++ b/server/einterfaces/metrics.go @@ -108,16 +108,16 @@ type MetricsInterface interface { ObserveClientTimeToLastByte(platform, agent, userID string, elapsed float64) ObserveClientTimeToDomInteractive(platform, agent, userID string, elapsed float64) ObserveClientSplashScreenEnd(platform, agent, pageType, userID string, elapsed float64) - ObserveClientFirstContentfulPaint(platform, agent string, elapsed float64) - ObserveClientLargestContentfulPaint(platform, agent, region string, elapsed float64) - ObserveClientInteractionToNextPaint(platform, agent, interaction string, elapsed float64) - ObserveClientCumulativeLayoutShift(platform, agent string, elapsed float64) - IncrementClientLongTasks(platform, agent string, inc float64) + ObserveClientFirstContentfulPaint(platform, agent, userID string, elapsed float64) + ObserveClientLargestContentfulPaint(platform, agent, region, userID string, elapsed float64) + ObserveClientInteractionToNextPaint(platform, agent, interaction, userID string, elapsed float64) + ObserveClientCumulativeLayoutShift(platform, agent, userID string, elapsed float64) + IncrementClientLongTasks(platform, agent, userID string, inc float64) ObserveClientPageLoadDuration(platform, agent, userID string, elapsed float64) - ObserveClientChannelSwitchDuration(platform, agent, fresh string, elapsed float64) - ObserveClientTeamSwitchDuration(platform, agent, fresh string, elapsed float64) - ObserveClientRHSLoadDuration(platform, agent string, elapsed float64) - ObserveGlobalThreadsLoadDuration(platform, agent string, elapsed float64) + ObserveClientChannelSwitchDuration(platform, agent, fresh, userID string, elapsed float64) + ObserveClientTeamSwitchDuration(platform, agent, fresh, userID string, elapsed float64) + ObserveClientRHSLoadDuration(platform, agent, userID string, elapsed float64) + ObserveGlobalThreadsLoadDuration(platform, agent, userID string, elapsed float64) ObserveMobileClientLoadDuration(platform string, elapsed float64) ObserveMobileClientChannelSwitchDuration(platform string, elapsed float64) ObserveMobileClientTeamSwitchDuration(platform string, elapsed float64) diff --git a/server/einterfaces/mocks/MetricsInterface.go b/server/einterfaces/mocks/MetricsInterface.go index 95c2b6462f..b0919c55cf 100644 --- a/server/einterfaces/mocks/MetricsInterface.go +++ b/server/einterfaces/mocks/MetricsInterface.go @@ -78,9 +78,9 @@ func (_m *MetricsInterface) IncrementChannelIndexCounter() { _m.Called() } -// IncrementClientLongTasks provides a mock function with given fields: platform, agent, inc -func (_m *MetricsInterface) IncrementClientLongTasks(platform string, agent string, inc float64) { - _m.Called(platform, agent, inc) +// IncrementClientLongTasks provides a mock function with given fields: platform, agent, userID, inc +func (_m *MetricsInterface) IncrementClientLongTasks(platform string, agent string, userID string, inc float64) { + _m.Called(platform, agent, userID, inc) } // IncrementClusterEventType provides a mock function with given fields: eventType @@ -303,29 +303,29 @@ func (_m *MetricsInterface) ObserveAPIEndpointDuration(endpoint string, method s _m.Called(endpoint, method, statusCode, originClient, pageLoadContext, elapsed) } -// ObserveClientChannelSwitchDuration provides a mock function with given fields: platform, agent, fresh, elapsed -func (_m *MetricsInterface) ObserveClientChannelSwitchDuration(platform string, agent string, fresh string, elapsed float64) { - _m.Called(platform, agent, fresh, elapsed) +// ObserveClientChannelSwitchDuration provides a mock function with given fields: platform, agent, fresh, userID, elapsed +func (_m *MetricsInterface) ObserveClientChannelSwitchDuration(platform string, agent string, fresh string, userID string, elapsed float64) { + _m.Called(platform, agent, fresh, userID, elapsed) } -// ObserveClientCumulativeLayoutShift provides a mock function with given fields: platform, agent, elapsed -func (_m *MetricsInterface) ObserveClientCumulativeLayoutShift(platform string, agent string, elapsed float64) { - _m.Called(platform, agent, elapsed) +// ObserveClientCumulativeLayoutShift provides a mock function with given fields: platform, agent, userID, elapsed +func (_m *MetricsInterface) ObserveClientCumulativeLayoutShift(platform string, agent string, userID string, elapsed float64) { + _m.Called(platform, agent, userID, elapsed) } -// ObserveClientFirstContentfulPaint provides a mock function with given fields: platform, agent, elapsed -func (_m *MetricsInterface) ObserveClientFirstContentfulPaint(platform string, agent string, elapsed float64) { - _m.Called(platform, agent, elapsed) +// ObserveClientFirstContentfulPaint provides a mock function with given fields: platform, agent, userID, elapsed +func (_m *MetricsInterface) ObserveClientFirstContentfulPaint(platform string, agent string, userID string, elapsed float64) { + _m.Called(platform, agent, userID, elapsed) } -// ObserveClientInteractionToNextPaint provides a mock function with given fields: platform, agent, interaction, elapsed -func (_m *MetricsInterface) ObserveClientInteractionToNextPaint(platform string, agent string, interaction string, elapsed float64) { - _m.Called(platform, agent, interaction, elapsed) +// ObserveClientInteractionToNextPaint provides a mock function with given fields: platform, agent, interaction, userID, elapsed +func (_m *MetricsInterface) ObserveClientInteractionToNextPaint(platform string, agent string, interaction string, userID string, elapsed float64) { + _m.Called(platform, agent, interaction, userID, elapsed) } -// ObserveClientLargestContentfulPaint provides a mock function with given fields: platform, agent, region, elapsed -func (_m *MetricsInterface) ObserveClientLargestContentfulPaint(platform string, agent string, region string, elapsed float64) { - _m.Called(platform, agent, region, elapsed) +// ObserveClientLargestContentfulPaint provides a mock function with given fields: platform, agent, region, userID, elapsed +func (_m *MetricsInterface) ObserveClientLargestContentfulPaint(platform string, agent string, region string, userID string, elapsed float64) { + _m.Called(platform, agent, region, userID, elapsed) } // ObserveClientPageLoadDuration provides a mock function with given fields: platform, agent, userID, elapsed @@ -333,9 +333,9 @@ func (_m *MetricsInterface) ObserveClientPageLoadDuration(platform string, agent _m.Called(platform, agent, userID, elapsed) } -// ObserveClientRHSLoadDuration provides a mock function with given fields: platform, agent, elapsed -func (_m *MetricsInterface) ObserveClientRHSLoadDuration(platform string, agent string, elapsed float64) { - _m.Called(platform, agent, elapsed) +// ObserveClientRHSLoadDuration provides a mock function with given fields: platform, agent, userID, elapsed +func (_m *MetricsInterface) ObserveClientRHSLoadDuration(platform string, agent string, userID string, elapsed float64) { + _m.Called(platform, agent, userID, elapsed) } // ObserveClientSplashScreenEnd provides a mock function with given fields: platform, agent, pageType, userID, elapsed @@ -343,9 +343,9 @@ func (_m *MetricsInterface) ObserveClientSplashScreenEnd(platform string, agent _m.Called(platform, agent, pageType, userID, elapsed) } -// ObserveClientTeamSwitchDuration provides a mock function with given fields: platform, agent, fresh, elapsed -func (_m *MetricsInterface) ObserveClientTeamSwitchDuration(platform string, agent string, fresh string, elapsed float64) { - _m.Called(platform, agent, fresh, elapsed) +// ObserveClientTeamSwitchDuration provides a mock function with given fields: platform, agent, fresh, userID, elapsed +func (_m *MetricsInterface) ObserveClientTeamSwitchDuration(platform string, agent string, fresh string, userID string, elapsed float64) { + _m.Called(platform, agent, fresh, userID, elapsed) } // ObserveClientTimeToDomInteractive provides a mock function with given fields: platform, agent, userID, elapsed @@ -388,9 +388,9 @@ func (_m *MetricsInterface) ObserveFilesSearchDuration(elapsed float64) { _m.Called(elapsed) } -// ObserveGlobalThreadsLoadDuration provides a mock function with given fields: platform, agent, elapsed -func (_m *MetricsInterface) ObserveGlobalThreadsLoadDuration(platform string, agent string, elapsed float64) { - _m.Called(platform, agent, elapsed) +// ObserveGlobalThreadsLoadDuration provides a mock function with given fields: platform, agent, userID, elapsed +func (_m *MetricsInterface) ObserveGlobalThreadsLoadDuration(platform string, agent string, userID string, elapsed float64) { + _m.Called(platform, agent, userID, elapsed) } // ObserveMobileClientChannelSwitchDuration provides a mock function with given fields: platform, elapsed diff --git a/server/enterprise/metrics/metrics.go b/server/enterprise/metrics/metrics.go index c5491366e3..84bb6836be 100644 --- a/server/enterprise/metrics/metrics.go +++ b/server/enterprise/metrics/metrics.go @@ -55,6 +55,8 @@ type MetricsInterfaceImpl struct { Registry *prometheus.Registry + ClientSideUserIds map[string]bool + DbMasterConnectionsGauge prometheus.GaugeFunc DbReadConnectionsGauge prometheus.GaugeFunc DbSearchConnectionsGauge prometheus.GaugeFunc @@ -240,7 +242,7 @@ func init() { }) } -// New creates a new MetricsInterface. The driver and datasoruce parameters are added during +// New creates a new MetricsInterface. The driver and datasource parameters are added during // migrating configuration store to the new platform service. Once the store and license are migrated, // we will be able to remove server dependency and lean on platform service during initialization. func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterfaceImpl { @@ -248,6 +250,12 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf Platform: ps, } + // Initialize ClientSideUserIds map + m.ClientSideUserIds = make(map[string]bool) + for _, userId := range ps.Config().MetricsSettings.ClientSideUserIds { + m.ClientSideUserIds[userId] = true + } + m.Registry = prometheus.NewRegistry() options := collectors.ProcessCollectorOpts{ Namespace: MetricsNamespace, @@ -1196,7 +1204,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf Help: "Duration from when a browser starts to request a page from a server until when it starts to receive data in response (seconds)", ConstLabels: additionalLabels, }, - []string{"platform", "agent"}, + []string{"platform", "agent", "user_id"}, m.Platform.Log(), ) m.Registry.MustRegister(m.ClientTimeToFirstByte) @@ -1209,7 +1217,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf Help: "Duration from when a browser starts to request a page from a server until when it receives the last byte of the resource or immediately before the transport connection is closed, whichever comes first. (seconds)", ConstLabels: additionalLabels, }, - []string{"platform", "agent"}, + []string{"platform", "agent", "user_id"}, m.Platform.Log(), ) m.Registry.MustRegister(m.ClientTimeToLastByte) @@ -1223,7 +1231,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf Buckets: []float64{.1, .25, .5, 1, 2.5, 5, 7.5, 10, 12.5, 15}, ConstLabels: additionalLabels, }, - []string{"platform", "agent"}, + []string{"platform", "agent", "user_id"}, m.Platform.Log(), ) m.Registry.MustRegister(m.ClientTimeToDOMInteractive) @@ -1237,7 +1245,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf Buckets: []float64{.1, .25, .5, 1, 2.5, 5, 7.5, 10, 12.5, 15}, ConstLabels: additionalLabels, }, - []string{"platform", "agent", "page_type"}, + []string{"platform", "agent", "page_type", "user_id"}, m.Platform.Log(), ) m.Registry.MustRegister(m.ClientSplashScreenEnd) @@ -1253,7 +1261,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 15, 20}, ConstLabels: additionalLabels, }, - []string{"platform", "agent"}, + []string{"platform", "agent", "user_id"}, ) m.Registry.MustRegister(m.ClientFirstContentfulPaint) @@ -1268,7 +1276,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 15, 20}, ConstLabels: additionalLabels, }, - []string{"platform", "agent", "region"}, + []string{"platform", "agent", "region", "user_id"}, ) m.Registry.MustRegister(m.ClientLargestContentfulPaint) @@ -1280,7 +1288,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf Help: "Measure of how long it takes for a user to see the effects of clicking with a mouse, tapping with a touchscreen, or pressing a key on the keyboard (seconds)", ConstLabels: additionalLabels, }, - []string{"platform", "agent", "interaction"}, + []string{"platform", "agent", "interaction", "user_id"}, ) m.Registry.MustRegister(m.ClientInteractionToNextPaint) @@ -1292,7 +1300,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf Help: "Measure of how much a page's content shifts unexpectedly", ConstLabels: additionalLabels, }, - []string{"platform", "agent"}, + []string{"platform", "agent", "user_id"}, ) m.Registry.MustRegister(m.ClientCumulativeLayoutShift) @@ -1304,7 +1312,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf Help: "Counter of the number of times that the browser's main UI thread is blocked for more than 50ms by a single task", ConstLabels: additionalLabels, }, - []string{"platform", "agent"}, + []string{"platform", "agent", "user_id"}, ) m.Registry.MustRegister(m.ClientLongTasks) @@ -1317,7 +1325,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 20, 40}, ConstLabels: additionalLabels, }, - []string{"platform", "agent"}, + []string{"platform", "agent", "user_id"}, m.Platform.Log(), ) m.Registry.MustRegister(m.ClientPageLoadDuration) @@ -1330,7 +1338,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf Help: "Duration of the time taken from when a user clicks on a channel in the LHS to when posts in that channel become visible (seconds)", ConstLabels: additionalLabels, }, - []string{"platform", "agent", "fresh"}, + []string{"platform", "agent", "fresh", "user_id"}, ) m.Registry.MustRegister(m.ClientChannelSwitchDuration) @@ -1342,7 +1350,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf Help: "Duration of the time taken from when a user clicks on a team in the LHS to when posts in that team become visible (seconds)", ConstLabels: additionalLabels, }, - []string{"platform", "agent", "fresh"}, + []string{"platform", "agent", "fresh", "user_id"}, ) m.Registry.MustRegister(m.ClientTeamSwitchDuration) @@ -1354,7 +1362,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf Help: "Duration of the time taken from when a user clicks to open a thread in the RHS until when posts in that thread become visible (seconds)", ConstLabels: additionalLabels, }, - []string{"platform", "agent"}, + []string{"platform", "agent", "user_id"}, ) m.Registry.MustRegister(m.ClientRHSLoadDuration) @@ -1366,7 +1374,7 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf Help: "Duration of the time taken from when a user clicks to open Threads in the LHS until when the global threads view becomes visible (milliseconds)", ConstLabels: additionalLabels, }, - []string{"platform", "agent"}, + []string{"platform", "agent", "user_id"}, ) m.Registry.MustRegister(m.ClientGlobalThreadsLoadDuration) @@ -2024,63 +2032,81 @@ func (mi *MetricsInterfaceImpl) DecrementHTTPWebSockets(originClient string) { mi.HTTPWebsocketsGauge.With(prometheus.Labels{"origin_client": originClient}).Dec() } +func (mi *MetricsInterfaceImpl) getEffectiveUserID(userID string) string { + if mi.ClientSideUserIds[userID] { + return userID + } + return "" +} + func (mi *MetricsInterfaceImpl) ObserveClientTimeToFirstByte(platform, agent, userID string, elapsed float64) { - mi.ClientTimeToFirstByte.With(prometheus.Labels{"platform": platform, "agent": agent}, userID).Observe(elapsed) + effectiveUserID := mi.getEffectiveUserID(userID) + mi.ClientTimeToFirstByte.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}, userID).Observe(elapsed) } func (mi *MetricsInterfaceImpl) ObserveClientTimeToLastByte(platform, agent, userID string, elapsed float64) { - mi.ClientTimeToLastByte.With(prometheus.Labels{"platform": platform, "agent": agent}, userID).Observe(elapsed) + effectiveUserID := mi.getEffectiveUserID(userID) + mi.ClientTimeToLastByte.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}, userID).Observe(elapsed) } func (mi *MetricsInterfaceImpl) ObserveClientTimeToDomInteractive(platform, agent, userID string, elapsed float64) { - mi.ClientTimeToDOMInteractive.With(prometheus.Labels{"platform": platform, "agent": agent}, userID).Observe(elapsed) + effectiveUserID := mi.getEffectiveUserID(userID) + mi.ClientTimeToDOMInteractive.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}, userID).Observe(elapsed) } func (mi *MetricsInterfaceImpl) ObserveClientSplashScreenEnd(platform, agent, pageType, userID string, elapsed float64) { - mi.ClientSplashScreenEnd.With(prometheus.Labels{"platform": platform, "agent": agent, "page_type": pageType}, userID).Observe(elapsed) + effectiveUserID := mi.getEffectiveUserID(userID) + mi.ClientSplashScreenEnd.With(prometheus.Labels{"platform": platform, "agent": agent, "page_type": pageType, "user_id": effectiveUserID}, userID).Observe(elapsed) } -func (mi *MetricsInterfaceImpl) ObserveClientFirstContentfulPaint(platform, agent string, elapsed float64) { - mi.ClientFirstContentfulPaint.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed) +func (mi *MetricsInterfaceImpl) ObserveClientFirstContentfulPaint(platform, agent, userID string, elapsed float64) { + effectiveUserID := mi.getEffectiveUserID(userID) + mi.ClientFirstContentfulPaint.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}).Observe(elapsed) } -func (mi *MetricsInterfaceImpl) ObserveClientLargestContentfulPaint(platform, agent, region string, elapsed float64) { - mi.ClientLargestContentfulPaint.With(prometheus.Labels{"platform": platform, "agent": agent, "region": region}).Observe(elapsed) +func (mi *MetricsInterfaceImpl) ObserveClientLargestContentfulPaint(platform, agent, region, userID string, elapsed float64) { + effectiveUserID := mi.getEffectiveUserID(userID) + mi.ClientLargestContentfulPaint.With(prometheus.Labels{"platform": platform, "agent": agent, "region": region, "user_id": effectiveUserID}).Observe(elapsed) } -func (mi *MetricsInterfaceImpl) ObserveClientInteractionToNextPaint(platform, agent, interaction string, elapsed float64) { - mi.ClientInteractionToNextPaint.With(prometheus.Labels{"platform": platform, "agent": agent, "interaction": interaction}).Observe(elapsed) +func (mi *MetricsInterfaceImpl) ObserveClientInteractionToNextPaint(platform, agent, interaction, userID string, elapsed float64) { + effectiveUserID := mi.getEffectiveUserID(userID) + mi.ClientInteractionToNextPaint.With(prometheus.Labels{"platform": platform, "agent": agent, "interaction": interaction, "user_id": effectiveUserID}).Observe(elapsed) } -func (mi *MetricsInterfaceImpl) ObserveClientCumulativeLayoutShift(platform, agent string, elapsed float64) { - mi.ClientCumulativeLayoutShift.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed) +func (mi *MetricsInterfaceImpl) ObserveClientCumulativeLayoutShift(platform, agent, userID string, elapsed float64) { + effectiveUserID := mi.getEffectiveUserID(userID) + mi.ClientCumulativeLayoutShift.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}).Observe(elapsed) } -func (mi *MetricsInterfaceImpl) IncrementClientLongTasks(platform, agent string, inc float64) { - mi.ClientLongTasks.With(prometheus.Labels{"platform": platform, "agent": agent}).Add(inc) +func (mi *MetricsInterfaceImpl) IncrementClientLongTasks(platform, agent, userID string, inc float64) { + effectiveUserID := mi.getEffectiveUserID(userID) + mi.ClientLongTasks.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}).Add(inc) } func (mi *MetricsInterfaceImpl) ObserveClientPageLoadDuration(platform, agent, userID string, elapsed float64) { - mi.ClientPageLoadDuration.With(prometheus.Labels{ - "platform": platform, - "agent": agent, - }, userID).Observe(elapsed) + effectiveUserID := mi.getEffectiveUserID(userID) + mi.ClientPageLoadDuration.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}, userID).Observe(elapsed) } -func (mi *MetricsInterfaceImpl) ObserveClientChannelSwitchDuration(platform, agent, fresh string, elapsed float64) { - mi.ClientChannelSwitchDuration.With(prometheus.Labels{"platform": platform, "agent": agent, "fresh": fresh}).Observe(elapsed) +func (mi *MetricsInterfaceImpl) ObserveClientChannelSwitchDuration(platform, agent, fresh, userID string, elapsed float64) { + effectiveUserID := mi.getEffectiveUserID(userID) + mi.ClientChannelSwitchDuration.With(prometheus.Labels{"platform": platform, "agent": agent, "fresh": fresh, "user_id": effectiveUserID}).Observe(elapsed) } -func (mi *MetricsInterfaceImpl) ObserveClientTeamSwitchDuration(platform, agent, fresh string, elapsed float64) { - mi.ClientTeamSwitchDuration.With(prometheus.Labels{"platform": platform, "agent": agent, "fresh": fresh}).Observe(elapsed) +func (mi *MetricsInterfaceImpl) ObserveClientTeamSwitchDuration(platform, agent, fresh, userID string, elapsed float64) { + effectiveUserID := mi.getEffectiveUserID(userID) + mi.ClientTeamSwitchDuration.With(prometheus.Labels{"platform": platform, "agent": agent, "fresh": fresh, "user_id": effectiveUserID}).Observe(elapsed) } -func (mi *MetricsInterfaceImpl) ObserveClientRHSLoadDuration(platform, agent string, elapsed float64) { - mi.ClientRHSLoadDuration.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed) +func (mi *MetricsInterfaceImpl) ObserveClientRHSLoadDuration(platform, agent, userID string, elapsed float64) { + effectiveUserID := mi.getEffectiveUserID(userID) + mi.ClientRHSLoadDuration.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}).Observe(elapsed) } -func (mi *MetricsInterfaceImpl) ObserveGlobalThreadsLoadDuration(platform, agent string, elapsed float64) { - mi.ClientGlobalThreadsLoadDuration.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed) +func (mi *MetricsInterfaceImpl) ObserveGlobalThreadsLoadDuration(platform, agent, userID string, elapsed float64) { + effectiveUserID := mi.getEffectiveUserID(userID) + mi.ClientGlobalThreadsLoadDuration.With(prometheus.Labels{"platform": platform, "agent": agent, "user_id": effectiveUserID}).Observe(elapsed) } func (mi *MetricsInterfaceImpl) ObserveDesktopCpuUsage(platform, version, process string, usage float64) { diff --git a/server/i18n/en.json b/server/i18n/en.json index 781bae8c76..bf574dcf44 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -9076,6 +9076,14 @@ "id": "model.config.is_valid.message_export.global_relay.smtp_username.app_error", "translation": "Message export job GlobalRelaySettings.SmtpUsername must be set." }, + { + "id": "model.config.is_valid.metrics_client_side_user_id.app_error", + "translation": "Invalid client side user id: {{.Id}}" + }, + { + "id": "model.config.is_valid.metrics_client_side_user_ids.app_error", + "translation": "Number of elements in ClientSideUserIds {{.CurrentLength}} is higher than maximum limit of {{.MaxLength}}." + }, { "id": "model.config.is_valid.move_thread.domain_invalid.app_error", "translation": "Invalid domain for move thread settings" @@ -9426,7 +9434,7 @@ }, { "id": "model.incoming_hook.id.app_error", - "translation": "Invalid Id." + "translation": "Invalid Id: {{.Id}}." }, { "id": "model.incoming_hook.parse_data.app_error", diff --git a/server/public/model/config.go b/server/public/model/config.go index 161a614664..2e0236b566 100644 --- a/server/public/model/config.go +++ b/server/public/model/config.go @@ -1071,11 +1071,12 @@ func (s *ClusterSettings) SetDefaults() { } type MetricsSettings struct { - Enable *bool `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` - BlockProfileRate *int `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` - ListenAddress *string `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` // telemetry: none - EnableClientMetrics *bool `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` - EnableNotificationMetrics *bool `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` + Enable *bool `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` + BlockProfileRate *int `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` + ListenAddress *string `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` // telemetry: none + EnableClientMetrics *bool `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` + EnableNotificationMetrics *bool `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` + ClientSideUserIds []string `access:"environment_performance_monitoring,write_restrictable,cloud_restrictable"` // telemetry: none } func (s *MetricsSettings) SetDefaults() { @@ -1098,6 +1099,23 @@ func (s *MetricsSettings) SetDefaults() { if s.EnableNotificationMetrics == nil { s.EnableNotificationMetrics = NewPointer(true) } + + if s.ClientSideUserIds == nil { + s.ClientSideUserIds = []string{} + } +} + +func (s *MetricsSettings) isValid() *AppError { + const maxLength = 5 + if len(s.ClientSideUserIds) > maxLength { + return NewAppError("MetricsSettings.IsValid", "model.config.is_valid.metrics_client_side_user_ids.app_error", map[string]any{"MaxLength": maxLength, "CurrentLength": len(s.ClientSideUserIds)}, "", http.StatusBadRequest) + } + for _, id := range s.ClientSideUserIds { + if !IsValidId(id) { + return NewAppError("MetricsSettings.IsValid", "model.config.is_valid.metrics_client_side_user_id.app_error", map[string]any{"Id": id}, "", http.StatusBadRequest) + } + } + return nil } type ExperimentalSettings struct { @@ -3806,6 +3824,10 @@ func (o *Config) IsValid() *AppError { return NewAppError("Config.IsValid", "model.config.is_valid.cluster_email_batching.app_error", nil, "", http.StatusBadRequest) } + if appErr := o.MetricsSettings.isValid(); appErr != nil { + return appErr + } + if appErr := o.CacheSettings.isValid(); appErr != nil { return appErr } diff --git a/server/public/model/incoming_webhook.go b/server/public/model/incoming_webhook.go index 81b035434e..4fcdbf381e 100644 --- a/server/public/model/incoming_webhook.go +++ b/server/public/model/incoming_webhook.go @@ -66,7 +66,7 @@ type IncomingWebhooksWithCount struct { func (o *IncomingWebhook) IsValid() *AppError { if !IsValidId(o.Id) { - return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.id.app_error", nil, "", http.StatusBadRequest) + return NewAppError("IncomingWebhook.IsValid", "model.incoming_hook.id.app_error", map[string]any{"Id": o.Id}, "", http.StatusBadRequest) } if o.CreateAt == 0 { diff --git a/webapp/channels/src/components/admin_console/__snapshots__/client_side_userids_setting.test.tsx.snap b/webapp/channels/src/components/admin_console/__snapshots__/client_side_userids_setting.test.tsx.snap new file mode 100644 index 0000000000..9df71e9dba --- /dev/null +++ b/webapp/channels/src/components/admin_console/__snapshots__/client_side_userids_setting.test.tsx.snap @@ -0,0 +1,470 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/AdminConsole/ClientSideUserIdsSetting initial state with multiple items 1`] = ` + + + } + inputId="MySetting" + label={ + + } + setByEnv={false} + > +
+ +
+ + + +
+ + + Set the user ids you want to track for client side metrics. Separate values with a comma. + + +
+
+
+
+
+`; + +exports[`components/AdminConsole/ClientSideUserIdsSetting initial state with no items 1`] = ` + + + } + inputId="MySetting" + label={ + + } + setByEnv={false} + > +
+ +
+ + + +
+ + + Set the user ids you want to track for client side metrics. Separate values with a comma. + + +
+
+
+
+
+`; + +exports[`components/AdminConsole/ClientSideUserIdsSetting initial state with one item 1`] = ` + + + } + inputId="MySetting" + label={ + + } + setByEnv={false} + > +
+ +
+ + + +
+ + + Set the user ids you want to track for client side metrics. Separate values with a comma. + + +
+
+
+
+
+`; + +exports[`components/AdminConsole/ClientSideUserIdsSetting renders properly when disabled 1`] = ` + + + } + inputId="MySetting" + label={ + + } + setByEnv={false} + > +
+ +
+ + + +
+ + + Set the user ids you want to track for client side metrics. Separate values with a comma. + + +
+
+
+
+
+`; + +exports[`components/AdminConsole/ClientSideUserIdsSetting renders properly when set by environment variable 1`] = ` + + + } + inputId="MySetting" + label={ + + } + setByEnv={true} + > +
+ +
+ + + +
+ + + Set the user ids you want to track for client side metrics. Separate values with a comma. + + +
+ +
+ + + This setting has been set through an environment variable. It cannot be changed through the System Console. + + +
+
+
+
+
+
+`; diff --git a/webapp/channels/src/components/admin_console/admin_definition.tsx b/webapp/channels/src/components/admin_console/admin_definition.tsx index 47220aed71..ac5f0fed6d 100644 --- a/webapp/channels/src/components/admin_console/admin_definition.tsx +++ b/webapp/channels/src/components/admin_console/admin_definition.tsx @@ -49,6 +49,7 @@ import CompanyInfo, {searchableStrings as billingCompanyInfoSearchableStrings} f import CompanyInfoEdit from './billing/company_info_edit'; import BleveSettings, {searchableStrings as bleveSearchableStrings} from './bleve_settings'; import BrandImageSetting from './brand_image_setting/brand_image_setting'; +import ClientSideUserIdsSetting from './client_side_userids_setting'; import ClusterSettings, {searchableStrings as clusterSearchableStrings} from './cluster_settings'; import CustomEnableDisableGuestAccountsSetting from './custom_enable_disable_guest_accounts_setting'; import CustomTermsOfServiceSettings from './custom_terms_of_service_settings'; @@ -1963,6 +1964,15 @@ const AdminDefinition: AdminDefinitionType = { it.configIsFalse('MetricsSettings', 'Enable'), ), }, + { + type: 'custom', + key: 'MetricsSettings.ClientSideUserIds', + component: ClientSideUserIdsSetting, + isDisabled: it.any( + it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.PERFORMANCE_MONITORING)), + it.configIsFalse('MetricsSettings', 'EnableClientMetrics'), + ), + }, { type: 'text', key: 'MetricsSettings.ListenAddress', diff --git a/webapp/channels/src/components/admin_console/client_side_userids_setting.test.tsx b/webapp/channels/src/components/admin_console/client_side_userids_setting.test.tsx new file mode 100644 index 0000000000..f7632b7582 --- /dev/null +++ b/webapp/channels/src/components/admin_console/client_side_userids_setting.test.tsx @@ -0,0 +1,133 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; + +import {mountWithIntl} from 'tests/helpers/intl-test-helper'; + +import ClientSideUserIdsSetting from './client_side_userids_setting'; + +describe('components/AdminConsole/ClientSideUserIdsSetting', () => { + const baseProps = { + id: 'MySetting', + value: ['userid1', 'userid2'], + onChange: jest.fn(), + disabled: false, + setByEnv: false, + }; + + describe('initial state', () => { + test('with no items', () => { + const props = { + ...baseProps, + value: [], + }; + + const wrapper = mountWithIntl( + , + ); + expect(wrapper).toMatchSnapshot(); + + expect(wrapper.state('value')).toEqual(''); + }); + + test('with one item', () => { + const props = { + ...baseProps, + value: ['userid1'], + }; + + const wrapper = mountWithIntl( + , + ); + expect(wrapper).toMatchSnapshot(); + + expect(wrapper.state('value')).toEqual('userid1'); + }); + + test('with multiple items', () => { + const props = { + ...baseProps, + value: ['userid1', 'userid2', 'id3'], + }; + + const wrapper = mountWithIntl( + , + ); + expect(wrapper).toMatchSnapshot(); + + expect(wrapper.state('value')).toEqual('userid1,userid2,id3'); + }); + }); + + describe('onChange', () => { + test('called on change to empty', () => { + const props = { + ...baseProps, + onChange: jest.fn(), + }; + + const wrapper = mountWithIntl( + , + ); + + wrapper.find('input').simulate('change', {target: {value: ''}}); + + expect(props.onChange).toBeCalledWith(baseProps.id, []); + }); + + test('called on change to one item', () => { + const props = { + ...baseProps, + onChange: jest.fn(), + }; + + const wrapper = mountWithIntl( + , + ); + + wrapper.find('input').simulate('change', {target: {value: ' id2 '}}); + + expect(props.onChange).toBeCalledWith(baseProps.id, ['id2']); + }); + + test('called on change to two items', () => { + const props = { + ...baseProps, + onChange: jest.fn(), + }; + + const wrapper = mountWithIntl( + , + ); + + wrapper.find('input').simulate('change', {target: {value: 'id1, id99'}}); + + expect(props.onChange).toBeCalledWith(baseProps.id, ['id1', 'id99']); + }); + }); + + test('renders properly when disabled', () => { + const props = { + ...baseProps, + disabled: true, + }; + + const wrapper = mountWithIntl( + , + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('renders properly when set by environment variable', () => { + const props = { + ...baseProps, + setByEnv: true, + }; + + const wrapper = mountWithIntl( + , + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/webapp/channels/src/components/admin_console/client_side_userids_setting.tsx b/webapp/channels/src/components/admin_console/client_side_userids_setting.tsx new file mode 100644 index 0000000000..81422f4dca --- /dev/null +++ b/webapp/channels/src/components/admin_console/client_side_userids_setting.tsx @@ -0,0 +1,81 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import type {ChangeEvent} from 'react'; +import {defineMessage, FormattedMessage} from 'react-intl'; + +import LocalizedPlaceholderInput from 'components/localized_placeholder_input'; + +import Setting from './setting'; + +type Props = { + id: string; + value: string[]; + onChange: (id: string, valueAsArray: string[]) => void; + disabled: boolean; + setByEnv: boolean; +} + +type State = { + value: string; +} + +export default class ClientSideUserIdsSetting extends PureComponent { + constructor(props: Props) { + super(props); + + this.state = { + value: this.arrayToString(props.value), + }; + } + + stringToArray = (str: string): string[] => { + return str.split(',').map((s) => s.trim()).filter(Boolean); + }; + + arrayToString = (arr: string[]): string => { + return arr.join(','); + }; + + handleChange = (e: ChangeEvent): void => { + const valueAsArray = this.stringToArray(e.target.value); + + this.props.onChange(this.props.id, valueAsArray); + + this.setState({ + value: e.target.value, + }); + }; + + render() { + return ( + + } + helpText={ + + } + inputId={this.props.id} + setByEnv={this.props.setByEnv} + > + + + ); + } +} diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 9eebb9f073..bab9930e8b 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -656,6 +656,9 @@ "admin.customization.announcement.enableBannerTitle": "Enable System-wide Notifications:", "admin.customization.appDownloadLinkDesc": "Add a link to a download page for the Mattermost apps. When a link is present, an option to \"Download Mattermost Apps\" will be added in the Product Menu so users can find the download page. Leave this field blank to hide the option from the Product Menu.", "admin.customization.appDownloadLinkTitle": "Mattermost Apps Download Page Link:", + "admin.customization.clientSideUserIds": "Client side user ids:", + "admin.customization.clientSideUserIdsDesc": "Set the user ids you want to track for client side metrics. Separate values with a comma.", + "admin.customization.clientSideUserIdsPlaceholder": "E.g.: \"userid1,userid2\"", "admin.customization.customUrlSchemes": "Custom URL Schemes:", "admin.customization.customUrlSchemesDesc": "Allows message text to link if it begins with any of the comma-separated URL schemes listed. By default, the following schemes will create links: \"http\", \"https\", \"ftp\", \"tel\", and \"mailto\".", "admin.customization.customUrlSchemesPlaceholder": "E.g.: \"git,smtp\"",