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
```
Этот коммит содержится в:
Agniva De Sarker
2024-11-29 11:24:35 +05:30
коммит произвёл GitHub
родитель b33622e32c
Коммит 4ec4b4d525
12 изменённых файлов: 138 добавлений и 36 удалений

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

@@ -1,6 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Measure, measureAndReport} from 'utils/performance_telemetry';
import {isDesktopApp} from 'utils/user_agent';
const ANIMATION_CLASS_FOR_MATTERMOST_LOGO_HIDE = 'LoadingAnimation__compass-shrink';
@@ -99,7 +100,7 @@ export class InitialLoadingScreenClass {
/**
* The loading animations are always started as soon as the loading indicator is shown in the screen for the first time.
* But we still want to have this start method incase we need to start the loading animations manually any time.
* But we still want to have this start method in case we need to start the loading animations manually any time.
* If we do want to do that then we should remove the set timeout destroy call doing above.
*/
public start() {
@@ -115,7 +116,7 @@ export class InitialLoadingScreenClass {
this.loadingAnimationElement.className = LOADING_CLASS_FOR_ANIMATION;
}
public stop() {
public stop(pageType: string) {
if (!this.loadingScreenElement || !this.loadingAnimationElement) {
return;
}
@@ -124,6 +125,15 @@ export class InitialLoadingScreenClass {
this.loadingScreenElement.className = LOADING_COMPLETE_CLASS_FOR_SCREEN;
this.loadingAnimationElement.className = LOADING_COMPLETE_CLASS_FOR_ANIMATION;
measureAndReport({
name: Measure.SplashScreen,
startMark: 0,
canFail: false,
labels: {
page_type: pageType,
},
});
}
}

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

@@ -293,7 +293,7 @@ export default class Root extends React.PureComponent<Props, State> {
if (prevState.shouldMountAppRoutes === false && this.state.shouldMountAppRoutes === true) {
if (!doesRouteBelongToTeamControllerRoutes(this.props.location.pathname)) {
DesktopApp.reactAppInitialized();
InitialLoadingScreen.stop();
InitialLoadingScreen.stop('root');
}
}
}

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

@@ -55,7 +55,7 @@ function TeamController(props: Props) {
useTelemetryIdentitySync();
useEffect(() => {
InitialLoadingScreen.stop();
InitialLoadingScreen.stop('team_controller');
DesktopApp.reactAppInitialized();
async function fetchAllChannels() {
await props.fetchAllMyTeamsChannels();

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

@@ -13,6 +13,10 @@ export const enum Measure {
ChannelSwitch = 'channel_switch',
GlobalThreadsLoad = 'global_threads_load',
PageLoad = 'page_load',
TTFB = 'TTFB',
TTLB = 'TTLB',
SplashScreen = 'splash_screen',
DomInteractive = 'dom_interactive',
RhsLoad = 'rhs_load',
TeamSwitch = 'team_switch',
}
@@ -29,7 +33,7 @@ export function markAndReport(name: string): PerformanceMark {
* Measures the duration between two performance marks, schedules it to be reported to the server, and returns the
* PerformanceMeasure created by doing this. If endMark is omitted, the measure will measure the duration until now.
*
* If either the start or end mark does not exist, undefined will be returned and, if canFail is false, an error
* If either the start or end mark does not exist, undefined will be returned, and if canFail is false, an error
* will be logged.
*/
export function measureAndReport({
@@ -40,8 +44,8 @@ export function measureAndReport({
canFail = false,
}: {
name: string;
startMark: string;
endMark?: string;
startMark: string | DOMHighResTimeStamp;
endMark?: string | DOMHighResTimeStamp;
labels?: Record<string, string>;
canFail?: boolean;
}): PerformanceMeasure | undefined {

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

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import nock from 'nock';
import {onCLS, onFCP, onINP, onLCP, onTTFB} from 'web-vitals/attribution';
import {onCLS, onFCP, onINP, onLCP} from 'web-vitals/attribution';
import {Client4} from '@mattermost/client';
@@ -216,8 +216,6 @@ describe.skip('PerformanceReporter', () => {
onINPCallback({name: 'INP', value: 200});
const onLCPCallback = (onLCP as jest.Mock).mock.calls[0][0];
onLCPCallback({name: 'LCP', value: 2500, entries: []});
const onTTFBCallback = (onTTFB as jest.Mock).mock.calls[0][0];
onTTFBCallback({name: 'TTFB', value: 800});
await waitForReport();
@@ -234,10 +232,6 @@ describe.skip('PerformanceReporter', () => {
metric: 'LCP',
value: 2500,
},
{
metric: 'TTFB',
value: 800,
},
],
});

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

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import type {Store} from 'redux';
import {onCLS, onFCP, onINP, onLCP, onTTFB} from 'web-vitals/attribution';
import {onCLS, onFCP, onINP, onLCP} from 'web-vitals/attribution';
import type {INPMetricWithAttribution, LCPMetricWithAttribution, Metric} from 'web-vitals/attribution';
import type {Client4} from '@mattermost/client';
@@ -114,16 +114,15 @@ export default class PerformanceReporter {
entryTypes: observedEntryTypes,
});
// Record the page load separately because it arrived before we were observing and because you can't use
// Record the page navigation 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();
this.measurePageNavigation();
// Register handlers for standard metrics and Web Vitals
onCLS((metric) => this.handleWebVital(metric));
onFCP((metric) => this.handleWebVital(metric));
onINP((metric) => this.handleWebVital(metric));
onLCP((metric) => this.handleWebVital(metric));
onTTFB((metric) => this.handleWebVital(metric));
// Periodically send performance telemetry to the server, roughly every minute but with some randomness to
// avoid overloading the server every minute.
@@ -138,17 +137,35 @@ export default class PerformanceReporter {
}
}
private measurePageLoad() {
private measurePageNavigation() {
const entries = performance.getEntriesByType('navigation');
if (entries.length === 0) {
return;
}
const entry = entries[0];
const ts = Date.now();
this.histogramMeasures.push({
metric: Measure.TTFB,
value: entry.responseStart,
timestamp: ts,
});
this.histogramMeasures.push({
metric: Measure.TTLB,
value: entry.responseEnd,
timestamp: ts,
});
this.histogramMeasures.push({
metric: Measure.DomInteractive,
value: entry.domInteractive,
timestamp: ts,
});
this.histogramMeasures.push({
metric: Measure.PageLoad,
value: entries[0].duration,
timestamp: Date.now(),
value: entry.duration,
timestamp: ts,
});
}