MM-61886: Add actionable page navigation metrics (#29332)
Page load is one of the metrics that we track and present to MLT. However, in its current form, it is not very actionable because it also contains the network latency. We split the whole metric into these parts: startTime | responseStart = TTFB | responseEnd = TTLB | domInteractive = Start of processing phase | loadEventEnd = Load complete This gives us better visibility into exactly which phase in the load process is slow. I have experimented with other metrics like - domContentLoadedEventStart - domContentLoadedEventEnd - domComplete and observed that they do not have sufficient gaps in the timespan to have any relevance. Additionally, I have moved TTFB from being a web vitals metric to being tracked from the performance metrics to remain consistent with the other navigation metrics measured. Lastly, I took this chance to improve some of the validation errors that we threw to include more context into the input that was passed and why does it fail. This also meant that I had to change the tests to check for error strings rather than direct errors which is a bad thing, but I don't think it's worth the effort trying to have named error variables for all of them. https://mattermost.atlassian.net/browse/MM-61886 ```release-note NONE ```
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
b33622e32c
Коммит
4ec4b4d525
@@ -28,6 +28,15 @@ func (a *App) RegisterPerformanceReport(rctx request.CTX, report *model.Performa
|
||||
switch h.Metric {
|
||||
case model.ClientTimeToFirstByte:
|
||||
a.Metrics().ObserveClientTimeToFirstByte(commonLabels["platform"], commonLabels["agent"], h.Value/1000)
|
||||
case model.ClientTimeToLastByte:
|
||||
a.Metrics().ObserveClientTimeToLastByte(commonLabels["platform"], commonLabels["agent"], h.Value/1000)
|
||||
case model.ClientTimeToDOMInteractive:
|
||||
a.Metrics().ObserveClientTimeToDomInteractive(commonLabels["platform"], commonLabels["agent"], h.Value/1000)
|
||||
case model.ClientSplashScreenEnd:
|
||||
a.Metrics().ObserveClientSplashScreenEnd(commonLabels["platform"],
|
||||
commonLabels["agent"],
|
||||
h.GetLabelValue("page_type", model.AcceptedSplashScreenOrigins, "team_controller"),
|
||||
h.Value/1000)
|
||||
case model.ClientFirstContentfulPaint:
|
||||
a.Metrics().ObserveClientFirstContentfulPaint(commonLabels["platform"], commonLabels["agent"], h.Value/1000)
|
||||
case model.ClientLargestContentfulPaint:
|
||||
|
||||
@@ -105,6 +105,9 @@ type MetricsInterface interface {
|
||||
IncrementNotificationUnsupportedCounter(notificationType model.NotificationType, notSentReason model.NotificationReason, platform string)
|
||||
|
||||
ObserveClientTimeToFirstByte(platform, agent string, elapsed float64)
|
||||
ObserveClientTimeToLastByte(platform, agent string, elapsed float64)
|
||||
ObserveClientTimeToDomInteractive(platform, agent string, elapsed float64)
|
||||
ObserveClientSplashScreenEnd(platform, agent, pageType string, elapsed float64)
|
||||
ObserveClientFirstContentfulPaint(platform, agent string, elapsed float64)
|
||||
ObserveClientLargestContentfulPaint(platform, agent, region string, elapsed float64)
|
||||
ObserveClientInteractionToNextPaint(platform, agent, interaction string, elapsed float64)
|
||||
|
||||
@@ -338,16 +338,31 @@ func (_m *MetricsInterface) ObserveClientRHSLoadDuration(platform string, agent
|
||||
_m.Called(platform, agent, elapsed)
|
||||
}
|
||||
|
||||
// ObserveClientSplashScreenEnd provides a mock function with given fields: platform, agent, pageType, elapsed
|
||||
func (_m *MetricsInterface) ObserveClientSplashScreenEnd(platform string, agent string, pageType string, elapsed float64) {
|
||||
_m.Called(platform, agent, pageType, 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)
|
||||
}
|
||||
|
||||
// ObserveClientTimeToDomInteractive provides a mock function with given fields: platform, agent, elapsed
|
||||
func (_m *MetricsInterface) ObserveClientTimeToDomInteractive(platform string, agent string, elapsed float64) {
|
||||
_m.Called(platform, agent, elapsed)
|
||||
}
|
||||
|
||||
// ObserveClientTimeToFirstByte provides a mock function with given fields: platform, agent, elapsed
|
||||
func (_m *MetricsInterface) ObserveClientTimeToFirstByte(platform string, agent string, elapsed float64) {
|
||||
_m.Called(platform, agent, elapsed)
|
||||
}
|
||||
|
||||
// ObserveClientTimeToLastByte provides a mock function with given fields: platform, agent, elapsed
|
||||
func (_m *MetricsInterface) ObserveClientTimeToLastByte(platform string, agent string, elapsed float64) {
|
||||
_m.Called(platform, agent, elapsed)
|
||||
}
|
||||
|
||||
// ObserveClusterRequestDuration provides a mock function with given fields: elapsed
|
||||
func (_m *MetricsInterface) ObserveClusterRequestDuration(elapsed float64) {
|
||||
_m.Called(elapsed)
|
||||
|
||||
@@ -202,6 +202,9 @@ type MetricsInterfaceImpl struct {
|
||||
NotificationUnsupportedCounters *prometheus.CounterVec
|
||||
|
||||
ClientTimeToFirstByte *prometheus.HistogramVec
|
||||
ClientTimeToLastByte *prometheus.HistogramVec
|
||||
ClientTimeToDOMInteractive *prometheus.HistogramVec
|
||||
ClientSplashScreenEnd *prometheus.HistogramVec
|
||||
ClientFirstContentfulPaint *prometheus.HistogramVec
|
||||
ClientLargestContentfulPaint *prometheus.HistogramVec
|
||||
ClientInteractionToNextPaint *prometheus.HistogramVec
|
||||
@@ -1187,6 +1190,39 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
|
||||
)
|
||||
m.Registry.MustRegister(m.ClientTimeToFirstByte)
|
||||
|
||||
m.ClientTimeToLastByte = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: MetricsSubsystemClientsWeb,
|
||||
Name: "time_to_last_byte",
|
||||
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)",
|
||||
},
|
||||
[]string{"platform", "agent"},
|
||||
)
|
||||
m.Registry.MustRegister(m.ClientTimeToLastByte)
|
||||
|
||||
m.ClientTimeToDOMInteractive = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: MetricsSubsystemClientsWeb,
|
||||
Name: "dom_interactive",
|
||||
Help: "Duration from when a browser starts to request a page from a server until when it sets the document's readyState to interactive. (seconds)",
|
||||
},
|
||||
[]string{"platform", "agent"},
|
||||
)
|
||||
m.Registry.MustRegister(m.ClientTimeToDOMInteractive)
|
||||
|
||||
m.ClientSplashScreenEnd = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: MetricsSubsystemClientsWeb,
|
||||
Name: "splash_screen",
|
||||
Help: "Duration from when a browser starts to request a page from a server until when the splash screen ends. (seconds)",
|
||||
},
|
||||
[]string{"platform", "agent", "page_type"},
|
||||
)
|
||||
m.Registry.MustRegister(m.ClientSplashScreenEnd)
|
||||
|
||||
m.ClientFirstContentfulPaint = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
@@ -1838,6 +1874,18 @@ func (mi *MetricsInterfaceImpl) ObserveClientTimeToFirstByte(platform, agent str
|
||||
mi.ClientTimeToFirstByte.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
|
||||
}
|
||||
|
||||
func (mi *MetricsInterfaceImpl) ObserveClientTimeToLastByte(platform, agent string, elapsed float64) {
|
||||
mi.ClientTimeToLastByte.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
|
||||
}
|
||||
|
||||
func (mi *MetricsInterfaceImpl) ObserveClientTimeToDomInteractive(platform, agent string, elapsed float64) {
|
||||
mi.ClientTimeToDOMInteractive.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
|
||||
}
|
||||
|
||||
func (mi *MetricsInterfaceImpl) ObserveClientSplashScreenEnd(platform, agent, pageType string, elapsed float64) {
|
||||
mi.ClientSplashScreenEnd.With(prometheus.Labels{"platform": platform, "agent": agent, "page_type": pageType}).Observe(elapsed)
|
||||
}
|
||||
|
||||
func (mi *MetricsInterfaceImpl) ObserveClientFirstContentfulPaint(platform, agent string, elapsed float64) {
|
||||
mi.ClientFirstContentfulPaint.With(prometheus.Labels{"platform": platform, "agent": agent}).Observe(elapsed)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ package model
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
)
|
||||
@@ -15,6 +14,9 @@ type MetricType string
|
||||
|
||||
const (
|
||||
ClientTimeToFirstByte MetricType = "TTFB"
|
||||
ClientTimeToLastByte MetricType = "TTLB"
|
||||
ClientTimeToDOMInteractive MetricType = "dom_interactive"
|
||||
ClientSplashScreenEnd MetricType = "splash_screen"
|
||||
ClientFirstContentfulPaint MetricType = "FCP"
|
||||
ClientLargestContentfulPaint MetricType = "LCP"
|
||||
ClientInteractionToNextPaint MetricType = "INP"
|
||||
@@ -54,7 +56,8 @@ var (
|
||||
"modal_content",
|
||||
"other",
|
||||
)
|
||||
AcceptedTrueFalseLabels = sliceToMapKey("true", "false")
|
||||
AcceptedTrueFalseLabels = sliceToMapKey("true", "false")
|
||||
AcceptedSplashScreenOrigins = sliceToMapKey("root", "team_controller")
|
||||
)
|
||||
|
||||
type MetricSample struct {
|
||||
@@ -86,7 +89,7 @@ func (r *PerformanceReport) IsValid() error {
|
||||
|
||||
reportVersion, err := semver.ParseTolerant(r.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("could not parse semver version: %s, %w", r.Version, err)
|
||||
}
|
||||
|
||||
if reportVersion.Major != performanceReportVersion.Major || reportVersion.Minor > performanceReportVersion.Minor {
|
||||
@@ -94,12 +97,12 @@ func (r *PerformanceReport) IsValid() error {
|
||||
}
|
||||
|
||||
if r.Start > r.End {
|
||||
return fmt.Errorf("report timestamps are erroneous")
|
||||
return fmt.Errorf("report timestamps are erroneous: start_timestamp %f is greater than end_timestamp %f", r.Start, r.End)
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
now := GetMillis()
|
||||
if r.End < float64(now-performanceReportTTLMilliseconds) {
|
||||
return fmt.Errorf("report is outdated: %f", r.End)
|
||||
return fmt.Errorf("report is outdated: end_time %f is past %d ms from now", r.End, performanceReportTTLMilliseconds)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -16,7 +15,7 @@ func TestPerformanceReport_IsValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
report *PerformanceReport
|
||||
expected error
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "ValidReport",
|
||||
@@ -26,12 +25,12 @@ func TestPerformanceReport_IsValid(t *testing.T) {
|
||||
Start: float64(time.Now().UnixMilli() - 10000),
|
||||
End: float64(time.Now().UnixMilli()),
|
||||
},
|
||||
expected: nil,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "NilReport",
|
||||
report: nil,
|
||||
expected: fmt.Errorf("the report is nil"),
|
||||
expected: "the report is nil",
|
||||
},
|
||||
{
|
||||
name: "UnsupportedVersion",
|
||||
@@ -41,7 +40,7 @@ func TestPerformanceReport_IsValid(t *testing.T) {
|
||||
Start: float64(time.Now().UnixMilli() - 10000),
|
||||
End: float64(time.Now().UnixMilli()),
|
||||
},
|
||||
expected: fmt.Errorf("report version is not supported: server version: 0.1.0, report version: 2.0.0"),
|
||||
expected: "report version is not supported:",
|
||||
},
|
||||
{
|
||||
name: "ErroneousTimestamps",
|
||||
@@ -51,7 +50,7 @@ func TestPerformanceReport_IsValid(t *testing.T) {
|
||||
Start: float64(time.Now().UnixMilli()),
|
||||
End: float64(time.Now().Add(-1 * time.Hour).UnixMilli()),
|
||||
},
|
||||
expected: fmt.Errorf("report timestamps are erroneous"),
|
||||
expected: "report timestamps are erroneous",
|
||||
},
|
||||
{
|
||||
name: "OutdatedReport",
|
||||
@@ -61,15 +60,15 @@ func TestPerformanceReport_IsValid(t *testing.T) {
|
||||
Start: float64(time.Now().Add(-7 * time.Minute).UnixMilli()),
|
||||
End: float64(outdatedTimestamp),
|
||||
},
|
||||
expected: fmt.Errorf("report is outdated: %f", float64(outdatedTimestamp)),
|
||||
expected: "report is outdated:",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.report.IsValid()
|
||||
if tt.expected != nil {
|
||||
require.EqualError(t, err, tt.expected.Error())
|
||||
if tt.expected != "" {
|
||||
require.Contains(t, err.Error(), tt.expected)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
Ссылка в новой задаче
Block a user