diff --git a/app/managers/performance_metrics_manager/index.test.ts b/app/managers/performance_metrics_manager/index.test.ts index 14263578a92..4dbe16dc50b 100644 --- a/app/managers/performance_metrics_manager/index.test.ts +++ b/app/managers/performance_metrics_manager/index.test.ts @@ -1,6 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import RNUtils from '@mattermost/rnutils'; import performance from 'react-native-performance'; import {mockApiClient} from '@test/mock_api_client'; @@ -64,6 +65,12 @@ describe('load metrics', () => { 'setInterval', ]}).setSystemTime(new Date(TEST_EPOCH)); PerformanceMetricsManager = new PerformanceMetricsManagerClass(); + + const mockHasRegisteredLoad = {hasRegisteredLoad: false}; + jest.mocked(RNUtils.setHasRegisteredLoad).mockImplementation(() => { + mockHasRegisteredLoad.hasRegisteredLoad = true; + }); + jest.mocked(RNUtils.getHasRegisteredLoad).mockImplementation(() => mockHasRegisteredLoad); }); afterEach(async () => { jest.useRealTimers(); @@ -91,6 +98,26 @@ describe('load metrics', () => { expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl, expectedRequest); }); + it('only register load once', async () => { + performance.mark('nativeLaunchStart'); + const measure = getMeasure(TEST_EPOCH, 0); + const expectedRequest = getBaseReportRequest(measure.timestamp, measure.timestamp + 1); + expectedRequest.body.histograms = [measure]; + + PerformanceMetricsManager.setLoadTarget('HOME'); + PerformanceMetricsManager.finishLoad('HOME', serverUrl); + await TestHelper.tick(); + jest.advanceTimersByTime(INTERVAL_TIME); + await TestHelper.tick(); + expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl, expectedRequest); + mockApiClient.post.mockClear(); + PerformanceMetricsManager.finishLoad('HOME', serverUrl); + await TestHelper.tick(); + jest.advanceTimersByTime(INTERVAL_TIME); + await TestHelper.tick(); + expect(mockApiClient.post).not.toHaveBeenCalled(); + }); + it('retry if the mark is not yet present', async () => { const measure = getMeasure(TEST_EPOCH + (RETRY_TIME * 2), RETRY_TIME); const expectedRequest = getBaseReportRequest(measure.timestamp, measure.timestamp + 1); diff --git a/app/managers/performance_metrics_manager/index.ts b/app/managers/performance_metrics_manager/index.ts index f3dcf8298a7..c73619d64ae 100644 --- a/app/managers/performance_metrics_manager/index.ts +++ b/app/managers/performance_metrics_manager/index.ts @@ -1,6 +1,7 @@ // 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'; @@ -18,7 +19,6 @@ const MAX_RETRIES = 3; class PerformanceMetricsManager { private target: Target; private batchers: {[serverUrl: string]: Batcher} = {}; - private hasRegisteredLoad = false; private lastAppStateIsActive = AppState.currentState === 'active'; constructor() { @@ -49,7 +49,7 @@ class PerformanceMetricsManager { } public skipLoadMetric() { - this.hasRegisteredLoad = true; + RNUtils.setHasRegisteredLoad(); } public finishLoad(location: Target, serverUrl: string) { @@ -57,7 +57,7 @@ class PerformanceMetricsManager { } private finishLoadWithRetries(location: Target, serverUrl: string, retries: number) { - if (this.target !== location || this.hasRegisteredLoad) { + if (this.target !== location || RNUtils.getHasRegisteredLoad().hasRegisteredLoad) { return; } @@ -79,7 +79,7 @@ class PerformanceMetricsManager { logWarning('We could not retrieve the mobile load metric'); } - this.hasRegisteredLoad = true; + RNUtils.setHasRegisteredLoad(); } public startMetric(metricName: MetricName) { diff --git a/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/RNUtilsModuleImpl.kt b/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/RNUtilsModuleImpl.kt index e440ac608ed..d79ae9bef68 100644 --- a/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/RNUtilsModuleImpl.kt +++ b/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/RNUtilsModuleImpl.kt @@ -17,6 +17,7 @@ class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) { const val NAME = "RNUtils" private var context: ReactApplicationContext? = null + private var hasRegisteredLoad = false fun sendJSEvent(eventName: String, data: ReadableMap?) { if (context?.hasActiveReactInstance() == true) { @@ -73,6 +74,16 @@ class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) { return SplitView.getWindowDimensions() } + fun setHasRegisteredLoad() { + hasRegisteredLoad = true + } + + fun getHasRegisteredLoad(): WritableMap { + val map = Arguments.createMap() + map.putBoolean("hasRegisteredLoad", hasRegisteredLoad) + return map + } + fun unlockOrientation() {} fun lockPortrait() {} diff --git a/libraries/@mattermost/rnutils/android/src/newarch/RNUtilsModule.kt b/libraries/@mattermost/rnutils/android/src/newarch/RNUtilsModule.kt index 412265c2942..a5f8c369c26 100644 --- a/libraries/@mattermost/rnutils/android/src/newarch/RNUtilsModule.kt +++ b/libraries/@mattermost/rnutils/android/src/newarch/RNUtilsModule.kt @@ -29,6 +29,9 @@ class RNUtilsModule(val reactContext: ReactApplicationContext) : NativeRNUtilsSp override fun getWindowDimensions(): WritableMap? = implementation.getWindowDimensions() + override fun setHasRegisteredLoad() = implementation.setHasRegisteredLoad() + override fun getHasRegisteredLoad(): WritableMap = implementation.getHasRegisteredLoad() + override fun unlockOrientation() { implementation.unlockOrientation() } diff --git a/libraries/@mattermost/rnutils/android/src/oldarch/RNUtilsModule.kt b/libraries/@mattermost/rnutils/android/src/oldarch/RNUtilsModule.kt index 2ece72b4d83..e28b0ba95b1 100644 --- a/libraries/@mattermost/rnutils/android/src/oldarch/RNUtilsModule.kt +++ b/libraries/@mattermost/rnutils/android/src/oldarch/RNUtilsModule.kt @@ -44,6 +44,16 @@ class RNUtilsModule(context: ReactApplicationContext) : return implementation.getWindowDimensions() } + @ReactMethod + fun setHasRegisteredLoad() { + implementation.setHasRegisteredLoad() + } + + @ReactMethod(isBlockingSynchronousMethod = true) + fun getHasRegisteredLoad(): WritableMap { + return implementation.getHasRegisteredLoad() + } + @ReactMethod fun unlockOrientation() { implementation.unlockOrientation() diff --git a/libraries/@mattermost/rnutils/ios/RNUtils.mm b/libraries/@mattermost/rnutils/ios/RNUtils.mm index b0ccfd68edb..489d5f56756 100644 --- a/libraries/@mattermost/rnutils/ios/RNUtils.mm +++ b/libraries/@mattermost/rnutils/ios/RNUtils.mm @@ -76,6 +76,14 @@ - (NSDictionary *)constantsToExport { return [wrapper getWindowDimensions]; } +RCT_REMAP_BLOCKING_SYNCHRONOUS_METHOD(getHasRegisteredLoad, NSDictionary*, getLoad) { + return [wrapper getHasRegisteredLoad]; +} + +RCT_REMAP_METHOD(setHasRegisteredLoad, setLoad) { + [wrapper setHasRegisteredLoad]; +} + RCT_REMAP_METHOD(unlockOrientation, unlock) { [wrapper unlockOrientation]; } @@ -166,6 +174,14 @@ - (NSDictionary *)getWindowDimensions { return [wrapper getWindowDimensions]; } +- (NSDictionary *)getHasRegisteredLoad { + return [wrapper getHasRegisteredLoad]; +} + +- (void)setHasRegisteredLoad { + [wrapper setHasRegisteredLoad]; +} + - (void)lockPortrait { [wrapper lockOrientation]; } diff --git a/libraries/@mattermost/rnutils/ios/RNUtilsWrapper.swift b/libraries/@mattermost/rnutils/ios/RNUtilsWrapper.swift index 5e639e076cc..1851eda679e 100644 --- a/libraries/@mattermost/rnutils/ios/RNUtilsWrapper.swift +++ b/libraries/@mattermost/rnutils/ios/RNUtilsWrapper.swift @@ -3,6 +3,7 @@ import React @objc public class RNUtilsWrapper: NSObject { @objc public weak var delegate: RNUtilsDelegate? = nil + @objc private var hasRegisteredLoad = false deinit { DispatchQueue.main.async { @@ -251,6 +252,14 @@ import React group.wait() return dimensions } + + @objc public func setHasRegisteredLoad() { + hasRegisteredLoad = true + } + + @objc public func getHasRegisteredLoad() -> Dictionary { + return ["hasRegisteredLoad": hasRegisteredLoad] + } @objc public func unlockOrientation() { DispatchQueue.main.async { diff --git a/libraries/@mattermost/rnutils/src/NativeRNUtils.ts b/libraries/@mattermost/rnutils/src/NativeRNUtils.ts index a9a2ce5a236..7ab46c89ce2 100644 --- a/libraries/@mattermost/rnutils/src/NativeRNUtils.ts +++ b/libraries/@mattermost/rnutils/src/NativeRNUtils.ts @@ -41,6 +41,10 @@ export type WindowDimensionsChanged = Readonly<{ height: Double; }> +type HasRegisteredLoadResponse = { + hasRegisteredLoad: boolean; +} + export interface Spec extends TurboModule { readonly getConstants: () => Constants; @@ -55,6 +59,9 @@ export interface Spec extends TurboModule { unlockOrientation: () => void; lockPortrait: () => void; + getHasRegisteredLoad: () => HasRegisteredLoadResponse; + setHasRegisteredLoad: () => void; + deleteDatabaseDirectory: (databaseName: string, shouldRemoveDirectory: boolean) => DatabaseOperationResult; renameDatabase: (databaseName: string, newDatabaseName: string) => DatabaseOperationResult; deleteEntitiesFile: () => boolean; diff --git a/test/setup.ts b/test/setup.ts index 2243502e752..acb0f3b2c2d 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -145,6 +145,8 @@ jest.doMock('react-native', () => { addListener: jest.fn(), removeListeners: jest.fn(), isRunningInSplitView: jest.fn().mockReturnValue({isSplit: false, isTablet: false}), + getHasRegisteredLoad: jest.fn().mockReturnValue({hasRegisteredLoad: false}), + setHasRegisteredLoad: jest.fn(), getDeliveredNotifications: jest.fn().mockResolvedValue([]), removeChannelNotifications: jest.fn().mockImplementation(),