mattermost-mobile/app/managers/performance_metrics_manager/index.ts
Elias Nahum d1d3fda83a
Request metrics improvements (#8420)
* Network metrics

* update network-client ref

* fix unit tests

* missing catch error parsing

* Replace API's to fetch all teams and channels for a user
Update groupLabel to use a type definition
Include effective Latency and Average Speed in the metrics
Include parallel and sequential request counts

* update network library and fix tests

* feat(MM-61865): send network request telemetry data to the server (#8436)

* feat(MM-61865): send network info telemetry data

* unit testing

* fix latency vs effectiveLatency

* cleanup test

* fix failing tests

* fix spelling error

* fix failing test

* more cleanup

* fix: improve parallel requests calculation (#8439)

* fix: improve parallel requests calculation

* fix test issue

* multiple parallelGroups testing

* calculateAverageSpeedWithCategories test

* categorizedRequests in it's own describe

* clean up duplicate tests

* check for when groupLabel is non-existent

* a case when groupLabel is non-existent

* more testing with effective latency.

* resolveAndFlattenModels with a capital F

* add try..catch within resolveAndFlattenModels

* update groupLabel to fix failing lint

* fix linting issue due to unknown group label

* forgot to push changes

* fix the indentation problem again

* add env var option for COLLECT_NETWORK_METRICS

---------

Co-authored-by: Rahim Rahman <rahim.rahman@mattermost.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2025-01-16 13:55:29 -07:00

144 lines
4.5 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import RNUtils from '@mattermost/rnutils';
import {AppState, type AppStateStatus} from 'react-native';
import performance from 'react-native-performance';
import {logDebug, logWarning} from '@utils/log';
import Batcher from './performance_metrics_batcher';
import type {NetworkRequestMetrics} from './constant';
import type {MarkOptions} from 'react-native-performance/lib/typescript/performance';
type Target = 'HOME' | 'CHANNEL' | 'THREAD' | undefined;
type MetricName = 'mobile_channel_switch' |
'mobile_team_switch';
const RETRY_TIME = 100;
const MAX_RETRIES = 3;
class PerformanceMetricsManager {
private target: Target;
private batchers: {[serverUrl: string]: Batcher} = {};
private lastAppStateIsActive = AppState.currentState === 'active';
constructor() {
AppState.addEventListener('change', (appState: AppStateStatus) => this.onAppStateChange(appState));
}
private onAppStateChange(appState: AppStateStatus) {
const isAppStateActive = appState === 'active';
if (this.lastAppStateIsActive !== isAppStateActive && !isAppStateActive) {
for (const batcher of Object.values(this.batchers)) {
batcher.forceSend();
}
}
this.lastAppStateIsActive = isAppStateActive;
}
private ensureBatcher(serverUrl: string) {
if (this.batchers[serverUrl]) {
return this.batchers[serverUrl];
}
this.batchers[serverUrl] = new Batcher(serverUrl);
return this.batchers[serverUrl];
}
public setLoadTarget(target: Target) {
this.target = target;
}
public skipLoadMetric() {
RNUtils.setHasRegisteredLoad();
}
public finishLoad(location: Target, serverUrl: string) {
this.finishLoadWithRetries(location, serverUrl, 0);
}
private finishLoadWithRetries(location: Target, serverUrl: string, retries: number) {
if (this.target !== location || RNUtils.getHasRegisteredLoad().hasRegisteredLoad) {
return;
}
try {
const measure = performance.measure('mobile_load', 'nativeLaunchStart');
this.ensureBatcher(serverUrl).addToBatch({
metric: 'mobile_load',
value: measure.duration,
timestamp: Date.now(),
});
performance.clearMeasures('mobile_load');
} catch {
// There seems to be a race condition where in some scenarios, the mobile load
// mark does not exist. We add this to avoid crashes related to this.
if (retries < MAX_RETRIES) {
setTimeout(() => this.finishLoadWithRetries(location, serverUrl, retries + 1), RETRY_TIME);
return;
}
logWarning('We could not retrieve the mobile load metric');
}
RNUtils.setHasRegisteredLoad();
}
public startMetric(metricName: MetricName) {
performance.mark(metricName);
}
public endMetric(metricName: MetricName, serverUrl: string) {
const marks = performance.getEntriesByName(metricName, 'mark');
if (!marks.length) {
return;
}
const measureName = `${metricName}_measure`;
const measure = performance.measure(measureName, metricName);
this.ensureBatcher(serverUrl).addToBatch({
metric: metricName,
value: measure.duration,
timestamp: Date.now(),
});
performance.clearMarks(metricName);
performance.clearMeasures(measureName);
}
public startTimeToInteraction(options?: MarkOptions) {
performance.mark('tti', options);
}
public measureTimeToInteraction() {
try {
const result = performance.measure('TTI', 'tti');
performance.clearMarks('tti');
performance.clearMeasures('TTI');
logDebug('Time to Interaction', result.duration);
return result;
} catch {
return undefined;
}
}
public collectNetworkRequestData = (name: NetworkRequestMetrics, value: number, {serverUrl, groupLabel}: NetworkRequestDataOtherInfo) => {
this.ensureBatcher(serverUrl).addToBatch({
metric: name,
value,
timestamp: Date.now(),
label: {network_request_group: groupLabel},
});
};
}
export const testExports = {
PerformanceMetricsManager,
RETRY_TIME,
MAX_RETRIES,
};
export default new PerformanceMetricsManager();