diff --git a/server/channels/app/metrics.go b/server/channels/app/metrics.go index 7a57b159c9..172a4ba600 100644 --- a/server/channels/app/metrics.go +++ b/server/channels/app/metrics.go @@ -36,6 +36,8 @@ func (a *App) RegisterPerformanceReport(rctx request.CTX, report *model.Performa a.Metrics().ObserveClientInteractionToNextPaint(commonLabels["platform"], commonLabels["agent"], h.Value/1000) case model.ClientCumulativeLayoutShift: a.Metrics().ObserveClientCumulativeLayoutShift(commonLabels["platform"], commonLabels["agent"], h.Value) + case model.ClientPageLoadDuration: + a.Metrics().ObserveClientPageLoadDuration(commonLabels["platform"], commonLabels["agent"], h.Value/1000) case model.ClientChannelSwitchDuration: a.Metrics().ObserveClientChannelSwitchDuration(commonLabels["platform"], commonLabels["agent"], h.Value/1000) case model.ClientTeamSwitchDuration: diff --git a/server/einterfaces/metrics.go b/server/einterfaces/metrics.go index af7ec7ecdb..87eea5efc9 100644 --- a/server/einterfaces/metrics.go +++ b/server/einterfaces/metrics.go @@ -109,6 +109,7 @@ type MetricsInterface interface { ObserveClientInteractionToNextPaint(platform, agent string, elapsed float64) ObserveClientCumulativeLayoutShift(platform, agent string, elapsed float64) IncrementClientLongTasks(platform, agent string, inc float64) + ObserveClientPageLoadDuration(platform, agent string, elapsed float64) ObserveClientChannelSwitchDuration(platform, agent string, elapsed float64) ObserveClientTeamSwitchDuration(platform, agent string, elapsed float64) ObserveClientRHSLoadDuration(platform, agent string, elapsed float64) diff --git a/server/einterfaces/mocks/MetricsInterface.go b/server/einterfaces/mocks/MetricsInterface.go index c114fd375b..33414e1209 100644 --- a/server/einterfaces/mocks/MetricsInterface.go +++ b/server/einterfaces/mocks/MetricsInterface.go @@ -323,6 +323,11 @@ func (_m *MetricsInterface) ObserveClientLargestContentfulPaint(platform string, _m.Called(platform, agent, elapsed) } +// ObserveClientPageLoadDuration provides a mock function with given fields: platform, agent, elapsed +func (_m *MetricsInterface) ObserveClientPageLoadDuration(platform string, agent string, elapsed float64) { + _m.Called(platform, agent, 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) diff --git a/server/enterprise/metrics/metrics.go b/server/enterprise/metrics/metrics.go index 47e08162b0..3f1f0cb51c 100644 --- a/server/enterprise/metrics/metrics.go +++ b/server/enterprise/metrics/metrics.go @@ -217,6 +217,7 @@ type MetricsInterfaceImpl struct { ClientInteractionToNextPaint *prometheus.HistogramVec ClientCumulativeLayoutShift *prometheus.HistogramVec ClientLongTasks *prometheus.CounterVec + ClientPageLoadDuration *prometheus.HistogramVec ClientChannelSwitchDuration *prometheus.HistogramVec ClientTeamSwitchDuration *prometheus.HistogramVec ClientRHSLoadDuration *prometheus.HistogramVec @@ -1200,6 +1201,17 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf ) m.Registry.MustRegister(m.ClientLongTasks) + m.ClientPageLoadDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: MetricsNamespace, + Subsystem: MetricsSubsystemClientsWeb, + Name: "page_load", + Help: "The amount of time from when the browser starts loading the web app until when the web app's load event has finished (seconds)", + }, + []string{"platform", "agent"}, + ) + m.Registry.MustRegister(m.ClientPageLoadDuration) + m.ClientChannelSwitchDuration = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Namespace: MetricsNamespace, @@ -1726,6 +1738,10 @@ func (mi *MetricsInterfaceImpl) IncrementClientLongTasks(platform, agent string, mi.ClientLongTasks.With(prometheus.Labels{"platform": platform, "agent": agent}).Add(inc) } +func (mi *MetricsInterfaceImpl) ObserveClientPageLoadDuration(platform, agent string, elapsed float64) { + mi.ClientPageLoadDuration.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed) +} + func (mi *MetricsInterfaceImpl) ObserveClientChannelSwitchDuration(platform, agent string, elapsed float64) { mi.ClientChannelSwitchDuration.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed) } diff --git a/server/public/model/metrics.go b/server/public/model/metrics.go index 85383d0f04..beca80810c 100644 --- a/server/public/model/metrics.go +++ b/server/public/model/metrics.go @@ -20,6 +20,7 @@ const ( ClientInteractionToNextPaint MetricType = "INP" ClientCumulativeLayoutShift MetricType = "CLS" ClientLongTasks MetricType = "long_tasks" + ClientPageLoadDuration MetricType = "page_load" ClientChannelSwitchDuration MetricType = "channel_switch" ClientTeamSwitchDuration MetricType = "team_switch" ClientRHSLoadDuration MetricType = "rhs_load" diff --git a/webapp/channels/src/utils/performance_telemetry/index.ts b/webapp/channels/src/utils/performance_telemetry/index.ts index dc736ef5da..548ebe659d 100644 --- a/webapp/channels/src/utils/performance_telemetry/index.ts +++ b/webapp/channels/src/utils/performance_telemetry/index.ts @@ -12,6 +12,7 @@ export const enum Mark { export const enum Measure { ChannelSwitch = 'channel_switch', GlobalThreadsLoad = 'global_threads_load', + PageLoad = 'page_load', RhsLoad = 'rhs_load', TeamSwitch = 'team_switch', } diff --git a/webapp/channels/src/utils/performance_telemetry/reporter.ts b/webapp/channels/src/utils/performance_telemetry/reporter.ts index e244957a4a..87a47067ac 100644 --- a/webapp/channels/src/utils/performance_telemetry/reporter.ts +++ b/webapp/channels/src/utils/performance_telemetry/reporter.ts @@ -16,6 +16,8 @@ import type {PerformanceLongTaskTiming} from './long_task'; import type {PlatformLabel, UserAgentLabel} from './platform_detection'; import {getPlatformLabel, getUserAgentLabel} from './platform_detection'; +import {Measure} from '.'; + type PerformanceReportMeasure = { /** @@ -93,6 +95,10 @@ export default class PerformanceReporter { entryTypes: observedEntryTypes, }); + // Record the page load separately because it arrived before we were observing and because you can't use + // the buffered option for PerformanceObserver with multiple entry types. + this.measurePageLoad(); + // Register handlers for standard metrics and Web Vitals onCLS((metric) => this.handleWebVital(metric)); onFCP((metric) => this.handleWebVital(metric)); @@ -109,6 +115,20 @@ export default class PerformanceReporter { addEventListener('visibilitychange', this.handleVisibilityChange); } + private measurePageLoad() { + const entries = performance.getEntriesByType('navigation'); + + if (entries.length === 0) { + return; + } + + this.histogramMeasures.push({ + metric: Measure.PageLoad, + value: entries[0].duration, + timestamp: performance.timeOrigin + entries[0].startTime, + }); + } + /** * This method is for testing only because we can't clean up the callbacks registered with web-vitals. */