@grafana/faro-react-native 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -30
- package/android/build.gradle +20 -15
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashIds.kt +44 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashReporter.kt +134 -38
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashSessionStore.kt +433 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashTraceCache.kt +11 -2
- package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativeModule.kt +128 -17
- package/android/src/main/java/com/grafana/faro/reactnative/FaroSessionProcess.kt +88 -0
- package/android/src/main/java/com/grafana/faro/reactnative/FrameMonitor.kt +34 -42
- package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterBelowApi30Test.kt +21 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterTest.kt +59 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashSessionStoreTest.kt +268 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashTraceCacheTest.kt +52 -0
- package/android/src/test/java/com/grafana/faro/reactnative/FaroSessionProcessTest.kt +127 -0
- package/dist/cjs/config/getRNInstrumentations.js +6 -8
- package/dist/cjs/config/getRNInstrumentations.js.map +1 -1
- package/dist/cjs/config/sampling.js.map +1 -1
- package/dist/cjs/config/types.js.map +1 -1
- package/dist/cjs/dataCollection/DataCollectionPolicy.js +1 -2
- package/dist/cjs/dataCollection/DataCollectionPolicy.js.map +1 -1
- package/dist/cjs/generated/faroRNPackageMeta.js +1 -1
- package/dist/cjs/generated/faroRNPackageMeta.js.map +1 -1
- package/dist/cjs/index.js +8 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/instrumentations/anr/instrumentation.js +10 -1
- package/dist/cjs/instrumentations/anr/instrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/appState/index.js +8 -2
- package/dist/cjs/instrumentations/appState/index.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +19 -6
- package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +331 -0
- package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
- package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
- package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
- package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/crashReporting/types.js.map +1 -1
- package/dist/cjs/instrumentations/errors/index.js +8 -3
- package/dist/cjs/instrumentations/errors/index.js.map +1 -1
- package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js +20 -31
- package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
- package/dist/cjs/instrumentations/frameMonitoring/types.js.map +1 -1
- package/dist/cjs/instrumentations/performance/index.js +2 -6
- package/dist/cjs/instrumentations/performance/index.js.map +1 -1
- package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js +48 -0
- package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
- package/dist/cjs/instrumentations/session/directSessionActivity.js +37 -0
- package/dist/cjs/instrumentations/session/directSessionActivity.js.map +1 -0
- package/dist/cjs/instrumentations/session/index.js +214 -45
- package/dist/cjs/instrumentations/session/index.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionActivity.js +43 -0
- package/dist/cjs/instrumentations/session/sessionActivity.js.map +1 -0
- package/dist/cjs/instrumentations/session/sessionAttributes.js +8 -8
- package/dist/cjs/instrumentations/session/sessionAttributes.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +199 -24
- package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -13
- package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js +72 -0
- package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
- package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js +124 -96
- package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionManager/types.js.map +1 -1
- package/dist/cjs/instrumentations/session/sessionProcess.js +96 -0
- package/dist/cjs/instrumentations/session/sessionProcess.js.map +1 -0
- package/dist/cjs/instrumentations/startup/index.js +2 -6
- package/dist/cjs/instrumentations/startup/index.js.map +1 -1
- package/dist/cjs/navigation/utils.js +3 -1
- package/dist/cjs/navigation/utils.js.map +1 -1
- package/dist/cjs/transports/fetch/index.js.map +1 -1
- package/dist/cjs/transports/fetch/transport.js +56 -25
- package/dist/cjs/transports/fetch/transport.js.map +1 -1
- package/dist/cjs/transports/fetch/types.js.map +1 -1
- package/dist/cjs/transports/offline/ConnectivityService.js +1 -1
- package/dist/cjs/transports/offline/ConnectivityService.js.map +1 -1
- package/dist/cjs/transports/offline/OfflineCache.js +2 -2
- package/dist/cjs/transports/offline/OfflineCache.js.map +1 -1
- package/dist/cjs/transports/offline/transport.js +3 -4
- package/dist/cjs/transports/offline/transport.js.map +1 -1
- package/dist/cjs/userPersistence/UserPersistence.js +1 -2
- package/dist/cjs/userPersistence/UserPersistence.js.map +1 -1
- package/dist/esm/config/getRNInstrumentations.js +6 -8
- package/dist/esm/config/getRNInstrumentations.js.map +1 -1
- package/dist/esm/config/sampling.js.map +1 -1
- package/dist/esm/config/types.js.map +1 -1
- package/dist/esm/dataCollection/DataCollectionPolicy.js +1 -2
- package/dist/esm/dataCollection/DataCollectionPolicy.js.map +1 -1
- package/dist/esm/generated/faroRNPackageMeta.js +1 -1
- package/dist/esm/generated/faroRNPackageMeta.js.map +1 -1
- package/dist/esm/index.js +4 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/instrumentations/anr/instrumentation.js +6 -1
- package/dist/esm/instrumentations/anr/instrumentation.js.map +1 -1
- package/dist/esm/instrumentations/appState/index.js +8 -2
- package/dist/esm/instrumentations/appState/index.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +15 -4
- package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +230 -0
- package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
- package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
- package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
- package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
- package/dist/esm/instrumentations/crashReporting/types.js.map +1 -1
- package/dist/esm/instrumentations/errors/index.js +8 -3
- package/dist/esm/instrumentations/errors/index.js.map +1 -1
- package/dist/esm/instrumentations/frameMonitoring/instrumentation.js +17 -32
- package/dist/esm/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
- package/dist/esm/instrumentations/frameMonitoring/types.js.map +1 -1
- package/dist/esm/instrumentations/performance/index.js +2 -6
- package/dist/esm/instrumentations/performance/index.js.map +1 -1
- package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js +34 -0
- package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
- package/dist/esm/instrumentations/session/directSessionActivity.js +32 -0
- package/dist/esm/instrumentations/session/directSessionActivity.js.map +1 -0
- package/dist/esm/instrumentations/session/index.js +207 -46
- package/dist/esm/instrumentations/session/index.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionActivity.js +38 -0
- package/dist/esm/instrumentations/session/sessionActivity.js.map +1 -0
- package/dist/esm/instrumentations/session/sessionAttributes.js +7 -7
- package/dist/esm/instrumentations/session/sessionAttributes.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +190 -27
- package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -14
- package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js +56 -0
- package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
- package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js +1 -1
- package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js +120 -44
- package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionManager/types.js.map +1 -1
- package/dist/esm/instrumentations/session/sessionProcess.js +89 -0
- package/dist/esm/instrumentations/session/sessionProcess.js.map +1 -0
- package/dist/esm/instrumentations/startup/index.js +2 -6
- package/dist/esm/instrumentations/startup/index.js.map +1 -1
- package/dist/esm/navigation/utils.js +2 -1
- package/dist/esm/navigation/utils.js.map +1 -1
- package/dist/esm/transports/fetch/index.js.map +1 -1
- package/dist/esm/transports/fetch/transport.js +38 -14
- package/dist/esm/transports/fetch/transport.js.map +1 -1
- package/dist/esm/transports/fetch/types.js.map +1 -1
- package/dist/esm/transports/offline/ConnectivityService.js +1 -1
- package/dist/esm/transports/offline/ConnectivityService.js.map +1 -1
- package/dist/esm/transports/offline/OfflineCache.js +2 -2
- package/dist/esm/transports/offline/OfflineCache.js.map +1 -1
- package/dist/esm/transports/offline/transport.js +3 -4
- package/dist/esm/transports/offline/transport.js.map +1 -1
- package/dist/esm/userPersistence/UserPersistence.js +1 -2
- package/dist/esm/userPersistence/UserPersistence.js.map +1 -1
- package/dist/types/config/getRNInstrumentations.d.ts +1 -2
- package/dist/types/config/sampling.d.ts +0 -3
- package/dist/types/config/types.d.ts +5 -3
- package/dist/types/dataCollection/DataCollectionPolicy.d.ts +1 -2
- package/dist/types/generated/faroRNPackageMeta.d.ts +1 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/instrumentations/anr/instrumentation.d.ts +1 -1
- package/dist/types/instrumentations/appState/index.d.ts +1 -1
- package/dist/types/instrumentations/console/index.d.ts +1 -1
- package/dist/types/instrumentations/crashReporting/BaseCrashReportingInstrumentation.d.ts +14 -4
- package/dist/types/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.d.ts +1 -1
- package/dist/types/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.d.ts +27 -0
- package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +3 -3
- package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +3 -3
- package/dist/types/instrumentations/crashReporting/types.d.ts +6 -0
- package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +3 -3
- package/dist/types/instrumentations/frameMonitoring/types.d.ts +4 -6
- package/dist/types/instrumentations/http/index.d.ts +1 -1
- package/dist/types/instrumentations/performance/index.d.ts +1 -3
- package/dist/types/instrumentations/session/FaroSessionActivityBoundary.d.ts +8 -0
- package/dist/types/instrumentations/session/directSessionActivity.d.ts +13 -0
- package/dist/types/instrumentations/session/index.d.ts +21 -2
- package/dist/types/instrumentations/session/sessionActivity.d.ts +9 -0
- package/dist/types/instrumentations/session/sessionAttributes.d.ts +6 -3
- package/dist/types/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.d.ts +16 -3
- package/dist/types/instrumentations/session/sessionManager/VolatileSessionManager.d.ts +9 -3
- package/dist/types/instrumentations/session/sessionManager/persistentSessionRecord.d.ts +4 -0
- package/dist/types/instrumentations/session/sessionManager/sessionManagerUtils.d.ts +19 -7
- package/dist/types/instrumentations/session/sessionManager/types.d.ts +6 -0
- package/dist/types/instrumentations/session/sessionProcess.d.ts +17 -0
- package/dist/types/instrumentations/startup/index.d.ts +3 -7
- package/dist/types/instrumentations/userActions/index.d.ts +1 -1
- package/dist/types/instrumentations/view/index.d.ts +1 -1
- package/dist/types/instrumentations/xhr/index.d.ts +1 -1
- package/dist/types/navigation/utils.d.ts +1 -0
- package/dist/types/testUtils/mockFaroReactNativeTracing.d.ts +1 -1
- package/dist/types/transports/console/transport.d.ts +1 -1
- package/dist/types/transports/fetch/index.d.ts +1 -1
- package/dist/types/transports/fetch/transport.d.ts +9 -2
- package/dist/types/transports/fetch/types.d.ts +5 -0
- package/dist/types/transports/offline/ConnectivityService.d.ts +1 -1
- package/dist/types/transports/offline/OfflineCache.d.ts +2 -2
- package/dist/types/transports/offline/transport.d.ts +4 -5
- package/dist/types/userPersistence/UserPersistence.d.ts +1 -2
- package/ios/FaroCrashReporter.swift +214 -21
- package/ios/FaroReactNative.swift +24 -0
- package/ios/FaroReactNativeModule.h +2 -1
- package/ios/FaroReactNativeModule.mm +121 -6
- package/ios/RefreshRateVitals.swift +34 -38
- package/package.json +8 -6
- package/src/config/getRNInstrumentations.ts +6 -8
- package/src/config/makeRNConfig.test.ts +6 -0
- package/src/config/sampling.ts +0 -3
- package/src/config/types.ts +5 -3
- package/src/dataCollection/DataCollectionPolicy.ts +1 -2
- package/src/generated/faroRNPackageMeta.ts +1 -1
- package/src/index.ts +8 -2
- package/src/instrumentations/anr/instrumentation.ts +8 -1
- package/src/instrumentations/appState/index.ts +10 -2
- package/src/instrumentations/crashReporting/BaseCrashReportingInstrumentation.ts +28 -8
- package/src/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.ts +277 -0
- package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +2 -2
- package/src/instrumentations/crashReporting/instrumentation.test.ts +433 -5
- package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +2 -2
- package/src/instrumentations/crashReporting/types.ts +7 -0
- package/src/instrumentations/errors/index.ts +12 -4
- package/src/instrumentations/errors/instrumentation.test.ts +35 -0
- package/src/instrumentations/frameMonitoring/instrumentation.test.ts +155 -0
- package/src/instrumentations/frameMonitoring/instrumentation.ts +19 -38
- package/src/instrumentations/frameMonitoring/types.ts +4 -6
- package/src/instrumentations/performance/index.ts +2 -6
- package/src/instrumentations/session/FaroSessionActivityBoundary.test.ts +60 -0
- package/src/instrumentations/session/FaroSessionActivityBoundary.tsx +41 -0
- package/src/instrumentations/session/beforeSendHook.test.ts +246 -0
- package/src/instrumentations/session/directSessionActivity.test.ts +158 -0
- package/src/instrumentations/session/directSessionActivity.ts +36 -0
- package/src/instrumentations/session/index.ts +269 -46
- package/src/instrumentations/session/persistentColdStart.test.ts +196 -0
- package/src/instrumentations/session/sessionActivity.test.ts +68 -0
- package/src/instrumentations/session/sessionActivity.ts +58 -0
- package/src/instrumentations/session/sessionAttributes.test.ts +37 -2
- package/src/instrumentations/session/sessionAttributes.ts +12 -6
- package/src/instrumentations/session/sessionCleanup.test.ts +410 -0
- package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.test.ts +552 -0
- package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.ts +249 -33
- package/src/instrumentations/session/sessionManager/VolatileSessionManager.ts +38 -18
- package/src/instrumentations/session/sessionManager/persistentSessionRecord.test.ts +103 -0
- package/src/instrumentations/session/sessionManager/persistentSessionRecord.ts +78 -0
- package/src/instrumentations/session/sessionManager/sampling.test.ts +1 -1
- package/src/instrumentations/session/sessionManager/sessionConstants.ts +1 -1
- package/src/instrumentations/session/sessionManager/sessionManagerUtils.test.ts +256 -18
- package/src/instrumentations/session/sessionManager/sessionManagerUtils.ts +180 -39
- package/src/instrumentations/session/sessionManager/types.ts +8 -0
- package/src/instrumentations/session/sessionManagerInstance.test.ts +65 -0
- package/src/instrumentations/session/sessionProcess.test.ts +136 -0
- package/src/instrumentations/session/sessionProcess.ts +114 -0
- package/src/instrumentations/session/startNewSession.test.ts +337 -0
- package/src/instrumentations/startup/index.ts +2 -6
- package/src/navigation/utils.ts +3 -1
- package/src/transports/fetch/index.ts +6 -1
- package/src/transports/fetch/transport.test.ts +86 -2
- package/src/transports/fetch/transport.ts +38 -16
- package/src/transports/fetch/types.ts +7 -0
- package/src/transports/offline/ConnectivityService.ts +1 -1
- package/src/transports/offline/OfflineCache.ts +2 -2
- package/src/transports/offline/transport.ts +3 -4
- package/src/userPersistence/UserPersistence.ts +1 -2
- package/dist/cjs/utils/throttle.js +0 -42
- package/dist/cjs/utils/throttle.js.map +0 -1
- package/dist/esm/utils/throttle.js +0 -34
- package/dist/esm/utils/throttle.js.map +0 -1
- package/dist/types/utils/throttle.d.ts +0 -9
- package/src/utils/throttle.test.ts +0 -141
- package/src/utils/throttle.ts +0 -34
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { NativeModules, Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { initializeFaro, type MeasurementEvent, type TransportItem } from '@grafana/faro-core';
|
|
4
|
+
import { mockConfig, MockTransport } from '@grafana/faro-test-utils';
|
|
5
|
+
|
|
6
|
+
import { FrameMonitoringInstrumentation } from './instrumentation';
|
|
7
|
+
|
|
8
|
+
jest.mock('react-native', () => ({
|
|
9
|
+
Platform: { OS: 'android' },
|
|
10
|
+
NativeModules: {
|
|
11
|
+
FaroReactNativeModule: {
|
|
12
|
+
startFrameMonitoring: jest.fn(),
|
|
13
|
+
getFrameMetrics: jest.fn(),
|
|
14
|
+
getRefreshRate: jest.fn().mockResolvedValue(null),
|
|
15
|
+
stopFrameMonitoring: jest.fn(),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
NativeEventEmitter: jest.fn().mockImplementation(() => ({
|
|
19
|
+
addListener: jest.fn(() => ({ remove: jest.fn() })),
|
|
20
|
+
})),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const POLL_INTERVAL_MS = 1000;
|
|
24
|
+
|
|
25
|
+
function getNativeModule() {
|
|
26
|
+
return NativeModules.FaroReactNativeModule as {
|
|
27
|
+
startFrameMonitoring: jest.Mock;
|
|
28
|
+
getFrameMetrics: jest.Mock;
|
|
29
|
+
getRefreshRate: jest.Mock;
|
|
30
|
+
stopFrameMonitoring: jest.Mock;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getFrozenFrameMeasurements(transport: MockTransport): MeasurementEvent[] {
|
|
35
|
+
return transport.items
|
|
36
|
+
.map((item) => item as TransportItem<MeasurementEvent>)
|
|
37
|
+
.filter((item) => item.type === 'measurement' && item.payload.type === 'app_frozen_frame')
|
|
38
|
+
.map((item) => item.payload);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('FrameMonitoringInstrumentation', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
jest.clearAllMocks();
|
|
44
|
+
jest.useFakeTimers();
|
|
45
|
+
getNativeModule().getFrameMetrics.mockResolvedValue(null);
|
|
46
|
+
getNativeModule().getRefreshRate.mockResolvedValue(null);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
jest.useRealTimers();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('sends app_frozen_frame once per poll when frozen frames are reported', async () => {
|
|
54
|
+
const transport = new MockTransport();
|
|
55
|
+
getNativeModule().getFrameMetrics.mockResolvedValue({
|
|
56
|
+
refreshRate: 60,
|
|
57
|
+
slowFrames: 0,
|
|
58
|
+
frozenFrames: 2,
|
|
59
|
+
frozenDurationMs: 1550,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
initializeFaro(
|
|
63
|
+
mockConfig({
|
|
64
|
+
refreshRateVitals: true,
|
|
65
|
+
transports: [transport],
|
|
66
|
+
instrumentations: [new FrameMonitoringInstrumentation({ refreshRatePollingInterval: POLL_INTERVAL_MS })],
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
|
|
71
|
+
|
|
72
|
+
const measurements = getFrozenFrameMeasurements(transport);
|
|
73
|
+
expect(measurements).toHaveLength(1);
|
|
74
|
+
expect(measurements[0].values).toEqual({
|
|
75
|
+
frozen_frames: 2,
|
|
76
|
+
frozen_duration: 1550,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('skips app_frozen_frame when frozen frame count is zero', async () => {
|
|
81
|
+
const transport = new MockTransport();
|
|
82
|
+
getNativeModule().getFrameMetrics.mockResolvedValue({
|
|
83
|
+
refreshRate: 60,
|
|
84
|
+
slowFrames: 0,
|
|
85
|
+
frozenFrames: 0,
|
|
86
|
+
frozenDurationMs: 0,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
initializeFaro(
|
|
90
|
+
mockConfig({
|
|
91
|
+
refreshRateVitals: true,
|
|
92
|
+
transports: [transport],
|
|
93
|
+
instrumentations: [new FrameMonitoringInstrumentation({ refreshRatePollingInterval: POLL_INTERVAL_MS })],
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
|
|
98
|
+
|
|
99
|
+
expect(getFrozenFrameMeasurements(transport)).toHaveLength(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('does not send app_frozen_frame when duration is zero even if count is positive', async () => {
|
|
103
|
+
const transport = new MockTransport();
|
|
104
|
+
getNativeModule().getFrameMetrics.mockResolvedValue({
|
|
105
|
+
refreshRate: 60,
|
|
106
|
+
slowFrames: 0,
|
|
107
|
+
frozenFrames: 1,
|
|
108
|
+
frozenDurationMs: 0,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
initializeFaro(
|
|
112
|
+
mockConfig({
|
|
113
|
+
refreshRateVitals: true,
|
|
114
|
+
transports: [transport],
|
|
115
|
+
instrumentations: [new FrameMonitoringInstrumentation({ refreshRatePollingInterval: POLL_INTERVAL_MS })],
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
|
|
120
|
+
|
|
121
|
+
expect(getFrozenFrameMeasurements(transport)).toHaveLength(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('reports frozen frames through polling only on Android (no duplicate event path)', async () => {
|
|
125
|
+
const transport = new MockTransport();
|
|
126
|
+
getNativeModule()
|
|
127
|
+
.getFrameMetrics.mockResolvedValueOnce({
|
|
128
|
+
refreshRate: 60,
|
|
129
|
+
slowFrames: 0,
|
|
130
|
+
frozenFrames: 1,
|
|
131
|
+
frozenDurationMs: 850,
|
|
132
|
+
})
|
|
133
|
+
.mockResolvedValueOnce({
|
|
134
|
+
refreshRate: 60,
|
|
135
|
+
slowFrames: 0,
|
|
136
|
+
frozenFrames: 0,
|
|
137
|
+
frozenDurationMs: 0,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
initializeFaro(
|
|
141
|
+
mockConfig({
|
|
142
|
+
refreshRateVitals: true,
|
|
143
|
+
transports: [transport],
|
|
144
|
+
instrumentations: [new FrameMonitoringInstrumentation({ refreshRatePollingInterval: POLL_INTERVAL_MS })],
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
|
|
149
|
+
await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
|
|
150
|
+
|
|
151
|
+
expect(getFrozenFrameMeasurements(transport)).toHaveLength(1);
|
|
152
|
+
expect(Platform.OS).toBe('android');
|
|
153
|
+
expect(getNativeModule().startFrameMonitoring).toHaveBeenCalledTimes(1);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -7,11 +7,11 @@ import type { ReactNativeConfig } from '../../config/types';
|
|
|
7
7
|
import type { FrameMetrics, FrameMonitoringOptions } from './types';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Default configuration values
|
|
11
|
-
*
|
|
10
|
+
* Default configuration values. Frozen frame threshold aligns with Android Vitals
|
|
11
|
+
* and OpenTelemetry Android slow rendering instrumentation (700ms).
|
|
12
12
|
*/
|
|
13
13
|
const DEFAULT_TARGET_FPS = 60;
|
|
14
|
-
const DEFAULT_FROZEN_FRAME_THRESHOLD_MS =
|
|
14
|
+
const DEFAULT_FROZEN_FRAME_THRESHOLD_MS = 700;
|
|
15
15
|
const DEFAULT_REFRESH_RATE_POLLING_INTERVAL = 30000;
|
|
16
16
|
const DEFAULT_NORMALIZED_REFRESH_RATE = 60;
|
|
17
17
|
|
|
@@ -29,7 +29,7 @@ const DEFAULT_NORMALIZED_REFRESH_RATE = 60;
|
|
|
29
29
|
* - Reports meaningful performance degradation that affects user experience
|
|
30
30
|
*
|
|
31
31
|
* ## Frozen Frame Detection
|
|
32
|
-
* Frozen frames are individual frames exceeding `frozenFrameThresholdMs` (default
|
|
32
|
+
* Frozen frames are individual frames exceeding `frozenFrameThresholdMs` (default 700ms).
|
|
33
33
|
* These represent severe jank where the app appears unresponsive.
|
|
34
34
|
*
|
|
35
35
|
* Sends measurements via Faro API:
|
|
@@ -46,7 +46,7 @@ const DEFAULT_NORMALIZED_REFRESH_RATE = 60;
|
|
|
46
46
|
* instrumentations: [
|
|
47
47
|
* new FrameMonitoringInstrumentation({
|
|
48
48
|
* targetFps: 60,
|
|
49
|
-
* frozenFrameThresholdMs:
|
|
49
|
+
* frozenFrameThresholdMs: 700,
|
|
50
50
|
* refreshRatePollingInterval: 30000,
|
|
51
51
|
* }),
|
|
52
52
|
* ],
|
|
@@ -94,7 +94,7 @@ export class FrameMonitoringInstrumentation extends BaseInstrumentation {
|
|
|
94
94
|
// Start native frame monitoring with configuration
|
|
95
95
|
this.startNativeMonitoring(nativeModule);
|
|
96
96
|
|
|
97
|
-
// Set up
|
|
97
|
+
// Set up Android refresh rate events (throttled on native side); frozen/slow frames use polling
|
|
98
98
|
this.setupEventListeners(nativeModule);
|
|
99
99
|
|
|
100
100
|
// Set up polling for periodic metrics collection (iOS pattern)
|
|
@@ -127,35 +127,24 @@ export class FrameMonitoringInstrumentation extends BaseInstrumentation {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
private setupEventListeners(nativeModule: typeof NativeModules.FaroReactNativeModule): void {
|
|
130
|
-
// Android
|
|
131
|
-
|
|
132
|
-
if (Platform.OS === 'android') {
|
|
130
|
+
// Android: refresh rate only. Slow and frozen frames are polled (same as iOS) to avoid duplicate reports.
|
|
131
|
+
if (Platform.OS === 'android' && this.refreshRateVitalsEnabled) {
|
|
133
132
|
try {
|
|
134
133
|
this.eventEmitter = new NativeEventEmitter(nativeModule);
|
|
135
134
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this.handleFrozenFrame(data, 0);
|
|
143
|
-
} else {
|
|
144
|
-
this.handleFrozenFrame(data.count, data.durationMs || 0);
|
|
145
|
-
}
|
|
135
|
+
const refreshRateSubscription = this.eventEmitter.addListener('onRefreshRate', (...args: readonly Object[]) => {
|
|
136
|
+
const [refreshRate] = args;
|
|
137
|
+
|
|
138
|
+
if (typeof refreshRate !== 'number') {
|
|
139
|
+
this.logWarn('Ignoring onRefreshRate event without a numeric payload');
|
|
140
|
+
return;
|
|
146
141
|
}
|
|
147
|
-
);
|
|
148
|
-
this.eventSubscriptions.push(frozenFrameSubscription);
|
|
149
142
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
this.handleRefreshRate(refreshRate);
|
|
154
|
-
});
|
|
155
|
-
this.eventSubscriptions.push(refreshRateSubscription);
|
|
156
|
-
}
|
|
143
|
+
this.handleRefreshRate(refreshRate);
|
|
144
|
+
});
|
|
145
|
+
this.eventSubscriptions.push(refreshRateSubscription);
|
|
157
146
|
|
|
158
|
-
this.logDebug('Set up Android
|
|
147
|
+
this.logDebug('Set up Android refresh rate event listener');
|
|
159
148
|
} catch (error) {
|
|
160
149
|
this.logError('Failed to set up frame event listeners', error);
|
|
161
150
|
}
|
|
@@ -163,8 +152,7 @@ export class FrameMonitoringInstrumentation extends BaseInstrumentation {
|
|
|
163
152
|
}
|
|
164
153
|
|
|
165
154
|
private startPolling(): void {
|
|
166
|
-
// Both iOS and Android use polling
|
|
167
|
-
// This provides consistent behavior and avoids flooding with events
|
|
155
|
+
// Both iOS and Android use polling for slow and frozen frames (avoids duplicate reports on Android).
|
|
168
156
|
this.pollingIntervalId = setInterval(() => {
|
|
169
157
|
this.pollFrameMetrics();
|
|
170
158
|
}, this.options.refreshRatePollingInterval);
|
|
@@ -230,14 +218,7 @@ export class FrameMonitoringInstrumentation extends BaseInstrumentation {
|
|
|
230
218
|
}
|
|
231
219
|
|
|
232
220
|
private handleFrozenFrame(count: number, durationMs: number): void {
|
|
233
|
-
// Only send frozen frame events if duration is greater than 0
|
|
234
|
-
// This filters out any erroneous 0ms frozen frames
|
|
235
221
|
if (durationMs > 0) {
|
|
236
|
-
// 🔍 TEMP DEBUG LOG - Remove after analysis
|
|
237
|
-
console.log(
|
|
238
|
-
`[Faro DEBUG ${Platform.OS.toUpperCase()}] 🧊 SENDING frozen frame: count=${count}, duration=${durationMs}ms`
|
|
239
|
-
);
|
|
240
|
-
|
|
241
222
|
this.api.pushMeasurement(
|
|
242
223
|
{
|
|
243
224
|
type: 'app_frozen_frame',
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Configuration options for Frame Monitoring instrumentation.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* for these parameters (not configurable). Defaults here match Flutter's hardcoded values
|
|
6
|
-
* for cross-platform consistency.
|
|
4
|
+
* Frozen frame threshold (700ms).
|
|
7
5
|
*
|
|
8
6
|
* Note: To enable frame monitoring, set `refreshRateVitals: true` in ReactNativeConfig.
|
|
9
7
|
*/
|
|
@@ -11,14 +9,14 @@ export interface FrameMonitoringOptions {
|
|
|
11
9
|
/**
|
|
12
10
|
* Target frames per second for slow frame detection.
|
|
13
11
|
* Frames rendering below this threshold are considered "slow".
|
|
14
|
-
* Default: 60
|
|
12
|
+
* Default: 60
|
|
15
13
|
*/
|
|
16
14
|
targetFps?: number;
|
|
17
15
|
|
|
18
16
|
/**
|
|
19
17
|
* Threshold in milliseconds for detecting frozen frames.
|
|
20
18
|
* Frames taking longer than this are considered "frozen".
|
|
21
|
-
* Default:
|
|
19
|
+
* Default: 700ms (Android Vitals and OpenTelemetry Android slow rendering instrumentation)
|
|
22
20
|
*/
|
|
23
21
|
frozenFrameThresholdMs?: number;
|
|
24
22
|
|
|
@@ -31,7 +29,7 @@ export interface FrameMonitoringOptions {
|
|
|
31
29
|
/**
|
|
32
30
|
* Normalized refresh rate for ProMotion displays (120Hz, etc.).
|
|
33
31
|
* Used to normalize frame rates for high-refresh-rate displays to a standard baseline.
|
|
34
|
-
* Default: 60
|
|
32
|
+
* Default: 60
|
|
35
33
|
*/
|
|
36
34
|
normalizedRefreshRate?: number;
|
|
37
35
|
}
|
|
@@ -13,8 +13,6 @@ const { FaroReactNativeModule } = NativeModules;
|
|
|
13
13
|
* - iOS: task_info() for memory, host_statistics() for CPU
|
|
14
14
|
* - Android: /proc/[pid]/status for memory, /proc/[pid]/stat for CPU
|
|
15
15
|
*
|
|
16
|
-
* Implementation ported from Faro Flutter SDK with feature parity.
|
|
17
|
-
*
|
|
18
16
|
* **Key Features**:
|
|
19
17
|
* - ✅ NO manual setup required - OS tracks metrics automatically!
|
|
20
18
|
* - ✅ Periodic collection (default: every 30 seconds)
|
|
@@ -143,16 +141,14 @@ export class PerformanceInstrumentation extends BaseInstrumentation {
|
|
|
143
141
|
const cpuUsage = FaroReactNativeModule.getCpuUsage();
|
|
144
142
|
this.logDebug(`CPU: raw value = ${cpuUsage}`);
|
|
145
143
|
|
|
146
|
-
// Validate CPU usage
|
|
147
|
-
// Skip null, negative, or exactly 0 (baseline reading)
|
|
144
|
+
// Validate CPU usage. Skip null, negative, or exactly 0 (baseline reading).
|
|
148
145
|
if (cpuUsage == null || cpuUsage <= 0) {
|
|
149
146
|
this.logDebug('CPU: skipping - value is null or <= 0');
|
|
150
147
|
return;
|
|
151
148
|
}
|
|
152
149
|
|
|
153
150
|
this.logDebug(`CPU: pushing measurement with cpu_usage = ${cpuUsage}`);
|
|
154
|
-
//
|
|
155
|
-
// in multi-core scenarios where one core is maxed out
|
|
151
|
+
// Values >= 100 can be valid in multi-core scenarios where one core is maxed out.
|
|
156
152
|
this.api.pushMeasurement({
|
|
157
153
|
type: 'app_cpu_usage',
|
|
158
154
|
values: { cpu_usage: cpuUsage },
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, View } from 'react-native';
|
|
3
|
+
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
|
|
4
|
+
|
|
5
|
+
import * as directSessionActivity from './directSessionActivity';
|
|
6
|
+
import { FaroSessionActivityBoundary } from './FaroSessionActivityBoundary';
|
|
7
|
+
|
|
8
|
+
describe('FaroSessionActivityBoundary', () => {
|
|
9
|
+
let renderer: ReactTestRenderer | undefined;
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
if (renderer != null) {
|
|
13
|
+
const mountedRenderer = renderer;
|
|
14
|
+
act(() => mountedRenderer.unmount());
|
|
15
|
+
renderer = undefined;
|
|
16
|
+
}
|
|
17
|
+
jest.restoreAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders a boundary that records a captured touch without claiming the responder', async () => {
|
|
21
|
+
const notifySpy = jest.spyOn(directSessionActivity, 'notifySessionActivity').mockImplementation();
|
|
22
|
+
const child = React.createElement(Text, null, 'Press me');
|
|
23
|
+
const event = { nativeEvent: {} } as never;
|
|
24
|
+
await act(async () => {
|
|
25
|
+
renderer = create(React.createElement(FaroSessionActivityBoundary, null, child));
|
|
26
|
+
});
|
|
27
|
+
if (renderer == null) {
|
|
28
|
+
throw new Error('Expected the activity boundary to render.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const boundary = renderer.root.findByType(View);
|
|
32
|
+
const claimsResponder = boundary.props.onStartShouldSetResponderCapture(event);
|
|
33
|
+
|
|
34
|
+
expect(notifySpy).toHaveBeenCalledTimes(1);
|
|
35
|
+
expect(claimsResponder).toBe(false);
|
|
36
|
+
expect(renderer.root.findByType(Text).props.children).toBe('Press me');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('preserves a caller responder decision and layout override', async () => {
|
|
40
|
+
const onStartShouldSetResponderCapture = jest.fn(() => true);
|
|
41
|
+
const event = { nativeEvent: {} } as never;
|
|
42
|
+
await act(async () => {
|
|
43
|
+
renderer = create(
|
|
44
|
+
React.createElement(FaroSessionActivityBoundary, {
|
|
45
|
+
onStartShouldSetResponderCapture,
|
|
46
|
+
style: { flex: 0, height: 40 },
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
if (renderer == null) {
|
|
51
|
+
throw new Error('Expected the activity boundary to render.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const boundary = renderer.root.findByType(View);
|
|
55
|
+
|
|
56
|
+
expect(boundary.props.onStartShouldSetResponderCapture(event)).toBe(true);
|
|
57
|
+
expect(onStartShouldSetResponderCapture).toHaveBeenCalledWith(event);
|
|
58
|
+
expect(boundary.props.style).toEqual([{ flex: 1 }, { flex: 0, height: 40 }]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type PropsWithChildren, useCallback } from 'react';
|
|
2
|
+
import { type GestureResponderEvent, StyleSheet, View, type ViewProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { notifySessionActivity } from './directSessionActivity';
|
|
5
|
+
|
|
6
|
+
export type FaroSessionActivityBoundaryProps = PropsWithChildren<ViewProps>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Records touch activity for the React Native subtree without emitting a user
|
|
10
|
+
* action or taking ownership of the responder.
|
|
11
|
+
*/
|
|
12
|
+
export function FaroSessionActivityBoundary({
|
|
13
|
+
children,
|
|
14
|
+
onStartShouldSetResponderCapture,
|
|
15
|
+
style,
|
|
16
|
+
...viewProps
|
|
17
|
+
}: FaroSessionActivityBoundaryProps) {
|
|
18
|
+
const handleStartShouldSetResponderCapture = useCallback(
|
|
19
|
+
(event: GestureResponderEvent): boolean => {
|
|
20
|
+
notifySessionActivity();
|
|
21
|
+
return onStartShouldSetResponderCapture?.(event) ?? false;
|
|
22
|
+
},
|
|
23
|
+
[onStartShouldSetResponderCapture]
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<View
|
|
28
|
+
{...viewProps}
|
|
29
|
+
onStartShouldSetResponderCapture={handleStartShouldSetResponderCapture}
|
|
30
|
+
style={[styles.container, style]}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</View>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const styles = StyleSheet.create({
|
|
38
|
+
container: {
|
|
39
|
+
flex: 1,
|
|
40
|
+
},
|
|
41
|
+
});
|