@grafana/faro-react-native 1.3.1 → 1.4.1
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 +17 -12
- 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/makeRNConfig.js +5 -5
- package/dist/cjs/config/makeRNConfig.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/makeRNConfig.js +5 -5
- package/dist/esm/config/makeRNConfig.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/crashReporting/BaseCrashReportingInstrumentation.d.ts +14 -4
- package/dist/types/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.d.ts +27 -0
- package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +2 -2
- package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +2 -2
- package/dist/types/instrumentations/crashReporting/types.d.ts +6 -0
- package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +2 -2
- package/dist/types/instrumentations/frameMonitoring/types.d.ts +4 -6
- package/dist/types/instrumentations/performance/index.d.ts +0 -2
- 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 +20 -1
- 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 +2 -6
- package/dist/types/navigation/utils.d.ts +1 -0
- package/dist/types/transports/fetch/index.d.ts +1 -1
- package/dist/types/transports/fetch/transport.d.ts +8 -1
- 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 +3 -4
- 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 +7 -5
- package/src/config/getRNInstrumentations.ts +6 -8
- package/src/config/makeRNConfig.test.ts +6 -0
- package/src/config/makeRNConfig.ts +10 -9
- 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/initialize.test.ts +63 -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
|
@@ -1,10 +1,124 @@
|
|
|
1
1
|
import { type ExceptionEvent, initializeFaro, type TransportItem } from '@grafana/faro-core';
|
|
2
2
|
import { mockConfig, MockTransport } from '@grafana/faro-test-utils';
|
|
3
3
|
|
|
4
|
+
import { FetchTransport } from '../../transports/fetch';
|
|
5
|
+
|
|
6
|
+
import { AndroidCrashReportingInstrumentation } from './android/AndroidCrashReportingInstrumentation';
|
|
4
7
|
import { CrashReportingInstrumentation } from './index';
|
|
8
|
+
import { IosCrashReportingInstrumentation } from './ios/IosCrashReportingInstrumentation';
|
|
5
9
|
import type { CrashReport } from './types';
|
|
6
10
|
|
|
11
|
+
const CRASH_TRACE = [
|
|
12
|
+
"ABI: 'arm64'",
|
|
13
|
+
'backtrace:',
|
|
14
|
+
' #00 pc 0000000000001234 /data/app/lib/arm64-v8a/libappmodules.so (nativeCrash+8)',
|
|
15
|
+
].join('\n');
|
|
16
|
+
|
|
17
|
+
type NativeCrashModule = {
|
|
18
|
+
acknowledgeCrashReports: ReturnType<typeof jest.fn>;
|
|
19
|
+
getCrashReport?: ReturnType<typeof jest.fn>;
|
|
20
|
+
getPendingCrashReports: ReturnType<typeof jest.fn>;
|
|
21
|
+
recordCrashSessionContext: ReturnType<typeof jest.fn>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type TestableAndroidCrashInstrumentation = {
|
|
25
|
+
prepareCrashReporting: (nativeModule: NativeCrashModule) => void;
|
|
26
|
+
processCrashReports: (nativeModule: NativeCrashModule) => Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type TestableIosCrashInstrumentation = {
|
|
30
|
+
prepareCrashReporting: (nativeModule: NativeCrashModule) => void;
|
|
31
|
+
processCrashReports: (nativeModule: NativeCrashModule) => Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function createCrashReport(overrides: Partial<CrashReport> = {}): CrashReport {
|
|
35
|
+
return {
|
|
36
|
+
reportId: 'report-a',
|
|
37
|
+
reason: 'CRASH_NATIVE',
|
|
38
|
+
sessionId: 'session-a',
|
|
39
|
+
timestamp: Date.now() - 1000,
|
|
40
|
+
trace: CRASH_TRACE,
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createNativeCrashModule(
|
|
46
|
+
reports: Array<CrashReport | string>,
|
|
47
|
+
recordSessionContextResult: boolean | undefined = true
|
|
48
|
+
): NativeCrashModule {
|
|
49
|
+
return {
|
|
50
|
+
acknowledgeCrashReports: jest.fn().mockResolvedValue(undefined),
|
|
51
|
+
getPendingCrashReports: jest
|
|
52
|
+
.fn()
|
|
53
|
+
.mockResolvedValue(reports.map((report) => (typeof report === 'string' ? report : JSON.stringify(report)))),
|
|
54
|
+
recordCrashSessionContext: jest.fn().mockReturnValue(recordSessionContextResult),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function setupAndroidReplay(
|
|
59
|
+
reports: Array<CrashReport | string>,
|
|
60
|
+
beforeSend?: (item: TransportItem) => TransportItem | null,
|
|
61
|
+
recordSessionContextResult: boolean | undefined = true
|
|
62
|
+
) {
|
|
63
|
+
const transport = new FetchTransport({ url: 'http://example.com/collect' });
|
|
64
|
+
const instrumentation = new AndroidCrashReportingInstrumentation({ enabled: false });
|
|
65
|
+
const faro = initializeFaro(
|
|
66
|
+
mockConfig({
|
|
67
|
+
transports: [transport],
|
|
68
|
+
instrumentations: [instrumentation],
|
|
69
|
+
...(beforeSend ? { beforeSend } : {}),
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
faro.api.setSession({ id: 'session-b' });
|
|
73
|
+
|
|
74
|
+
const nativeModule = createNativeCrashModule(reports, recordSessionContextResult);
|
|
75
|
+
const sendSpy = jest.spyOn(transport, 'sendWithResult').mockResolvedValue({
|
|
76
|
+
outcome: 'accepted',
|
|
77
|
+
status: 202,
|
|
78
|
+
});
|
|
79
|
+
const testable = instrumentation as unknown as TestableAndroidCrashInstrumentation;
|
|
80
|
+
testable.prepareCrashReporting(nativeModule);
|
|
81
|
+
|
|
82
|
+
return { faro, instrumentation, nativeModule, sendSpy, testable };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function setupIosReplay(reports: Array<CrashReport | string>) {
|
|
86
|
+
const transport = new FetchTransport({ url: 'http://example.com/collect' });
|
|
87
|
+
const instrumentation = new IosCrashReportingInstrumentation({ enabled: false });
|
|
88
|
+
const faro = initializeFaro(
|
|
89
|
+
mockConfig({
|
|
90
|
+
transports: [transport],
|
|
91
|
+
instrumentations: [instrumentation],
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
faro.api.setSession({ id: 'session-b' });
|
|
95
|
+
|
|
96
|
+
const nativeModule = createNativeCrashModule(reports);
|
|
97
|
+
const sendSpy = jest.spyOn(transport, 'sendWithResult').mockResolvedValue({
|
|
98
|
+
outcome: 'accepted',
|
|
99
|
+
status: 202,
|
|
100
|
+
});
|
|
101
|
+
const testable = instrumentation as unknown as TestableIosCrashInstrumentation;
|
|
102
|
+
testable.prepareCrashReporting(nativeModule);
|
|
103
|
+
|
|
104
|
+
return { faro, instrumentation, nativeModule, sendSpy, testable };
|
|
105
|
+
}
|
|
106
|
+
|
|
7
107
|
describe('CrashReportingInstrumentation', () => {
|
|
108
|
+
const androidInstrumentations: AndroidCrashReportingInstrumentation[] = [];
|
|
109
|
+
const iosInstrumentations: IosCrashReportingInstrumentation[] = [];
|
|
110
|
+
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
jest.restoreAllMocks();
|
|
113
|
+
delete (global as any).faro;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
androidInstrumentations.splice(0).forEach((instrumentation) => instrumentation.unpatch());
|
|
118
|
+
iosInstrumentations.splice(0).forEach((instrumentation) => instrumentation.unpatch());
|
|
119
|
+
delete (global as any).faro;
|
|
120
|
+
});
|
|
121
|
+
|
|
8
122
|
it('should report native crashes as fatal exceptions', () => {
|
|
9
123
|
const transport = new MockTransport();
|
|
10
124
|
const instrumentation = new CrashReportingInstrumentation({ enabled: false });
|
|
@@ -24,11 +138,7 @@ describe('CrashReportingInstrumentation', () => {
|
|
|
24
138
|
reason: 'CRASH_NATIVE',
|
|
25
139
|
signal: 'SIGSEGV (11)',
|
|
26
140
|
timestamp: 1710000000000,
|
|
27
|
-
trace:
|
|
28
|
-
"ABI: 'arm64'",
|
|
29
|
-
'backtrace:',
|
|
30
|
-
' #00 pc 0000000000001234 /data/app/lib/arm64-v8a/libappmodules.so (nativeCrash+8)',
|
|
31
|
-
].join('\n'),
|
|
141
|
+
trace: CRASH_TRACE,
|
|
32
142
|
});
|
|
33
143
|
|
|
34
144
|
expect(transport.items).toHaveLength(1);
|
|
@@ -39,4 +149,322 @@ describe('CrashReportingInstrumentation', () => {
|
|
|
39
149
|
expect(item.payload.context?.signal).toBe('SIGSEGV (11)');
|
|
40
150
|
expect(item.payload.context?.trace).toContain('#00 pc');
|
|
41
151
|
});
|
|
152
|
+
|
|
153
|
+
it('replays a recovered crash with its original session and timestamp without changing the live session', async () => {
|
|
154
|
+
const crashTimestamp = Date.now() - 1000;
|
|
155
|
+
const setup = setupAndroidReplay([
|
|
156
|
+
createCrashReport({
|
|
157
|
+
appVersion: '1.2.3',
|
|
158
|
+
reportId: 'report-a',
|
|
159
|
+
sessionId: 'session-a',
|
|
160
|
+
timestamp: crashTimestamp,
|
|
161
|
+
}),
|
|
162
|
+
]);
|
|
163
|
+
androidInstrumentations.push(setup.instrumentation);
|
|
164
|
+
|
|
165
|
+
await setup.testable.processCrashReports(setup.nativeModule);
|
|
166
|
+
|
|
167
|
+
expect(setup.sendSpy).toHaveBeenCalledTimes(1);
|
|
168
|
+
const item = setup.sendSpy.mock.calls[0]?.[0]?.[0] as TransportItem<ExceptionEvent>;
|
|
169
|
+
expect(item.meta.session?.id).toBe('session-a');
|
|
170
|
+
expect(item.meta.app?.version).toBe('1.2.3');
|
|
171
|
+
expect(item.payload.timestamp).toBe(new Date(crashTimestamp).toISOString());
|
|
172
|
+
expect(item.payload.fatal).toBe(true);
|
|
173
|
+
expect(setup.faro.metas.value.session?.id).toBe('session-b');
|
|
174
|
+
expect(setup.nativeModule.acknowledgeCrashReports).toHaveBeenCalledWith(['report-a']);
|
|
175
|
+
expect(setup.nativeModule.recordCrashSessionContext).toHaveBeenCalledWith(
|
|
176
|
+
expect.objectContaining({ sessionId: 'session-b' })
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('retains the crash-time sampling decision across same-session metadata updates', () => {
|
|
181
|
+
const setup = setupAndroidReplay([]);
|
|
182
|
+
androidInstrumentations.push(setup.instrumentation);
|
|
183
|
+
|
|
184
|
+
setup.faro.api.setSession({
|
|
185
|
+
id: 'session-b',
|
|
186
|
+
attributes: { isSampled: 'false' },
|
|
187
|
+
});
|
|
188
|
+
const callsBeforeMetadataUpdate = setup.nativeModule.recordCrashSessionContext.mock.calls.length;
|
|
189
|
+
expect(setup.nativeModule.recordCrashSessionContext).toHaveBeenLastCalledWith(
|
|
190
|
+
expect.objectContaining({ sessionId: 'session-b', isSampled: false })
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
setup.faro.api.setSession({
|
|
194
|
+
id: 'session-b',
|
|
195
|
+
attributes: { custom: 'value' },
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(setup.nativeModule.recordCrashSessionContext).toHaveBeenCalledTimes(callsBeforeMetadataUpdate);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('does not carry a sampling decision into a different session', () => {
|
|
202
|
+
const setup = setupAndroidReplay([]);
|
|
203
|
+
androidInstrumentations.push(setup.instrumentation);
|
|
204
|
+
|
|
205
|
+
setup.faro.api.setSession({
|
|
206
|
+
id: 'session-b',
|
|
207
|
+
attributes: { isSampled: 'false' },
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
setup.faro.api.setSession({ id: 'session-c' });
|
|
211
|
+
|
|
212
|
+
const recordedContext = setup.nativeModule.recordCrashSessionContext.mock.lastCall?.[0];
|
|
213
|
+
expect(recordedContext).toEqual(
|
|
214
|
+
expect.objectContaining({
|
|
215
|
+
sessionId: 'session-c',
|
|
216
|
+
activatedAt: expect.any(Number),
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
expect(recordedContext).not.toHaveProperty('isSampled');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('replays an iOS crash with its original session and acknowledges it after delivery', async () => {
|
|
223
|
+
const crashTimestamp = Date.now() - 1000;
|
|
224
|
+
const setup = setupIosReplay([
|
|
225
|
+
createCrashReport({
|
|
226
|
+
reason: 'SIGSEGV',
|
|
227
|
+
reportId: 'ios-report-a',
|
|
228
|
+
sessionId: 'session-a',
|
|
229
|
+
timestamp: crashTimestamp,
|
|
230
|
+
}),
|
|
231
|
+
]);
|
|
232
|
+
iosInstrumentations.push(setup.instrumentation);
|
|
233
|
+
|
|
234
|
+
await setup.testable.processCrashReports(setup.nativeModule);
|
|
235
|
+
|
|
236
|
+
expect(setup.sendSpy).toHaveBeenCalledTimes(1);
|
|
237
|
+
const item = setup.sendSpy.mock.calls[0]?.[0]?.[0] as TransportItem<ExceptionEvent>;
|
|
238
|
+
expect(item.meta.session?.id).toBe('session-a');
|
|
239
|
+
expect(item.payload.timestamp).toBe(new Date(crashTimestamp).toISOString());
|
|
240
|
+
expect(setup.faro.metas.value.session?.id).toBe('session-b');
|
|
241
|
+
expect(setup.nativeModule.acknowledgeCrashReports).toHaveBeenCalledWith(['ios-report-a']);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('uses legacy iOS crash replay when the native module does not support acknowledgements', async () => {
|
|
245
|
+
const transport = new MockTransport();
|
|
246
|
+
const instrumentation = new IosCrashReportingInstrumentation({ enabled: false });
|
|
247
|
+
const faro = initializeFaro(
|
|
248
|
+
mockConfig({
|
|
249
|
+
transports: [transport],
|
|
250
|
+
instrumentations: [instrumentation],
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
faro.api.setSession({ id: 'session-b' });
|
|
254
|
+
iosInstrumentations.push(instrumentation);
|
|
255
|
+
|
|
256
|
+
const nativeModule = {
|
|
257
|
+
getCrashReport: jest.fn().mockResolvedValue([JSON.stringify(createCrashReport())]),
|
|
258
|
+
} as unknown as NativeCrashModule;
|
|
259
|
+
const testable = instrumentation as unknown as TestableIosCrashInstrumentation;
|
|
260
|
+
testable.prepareCrashReporting(nativeModule);
|
|
261
|
+
|
|
262
|
+
await testable.processCrashReports(nativeModule);
|
|
263
|
+
|
|
264
|
+
expect(nativeModule.getCrashReport).toHaveBeenCalledTimes(1);
|
|
265
|
+
expect(transport.items).toHaveLength(1);
|
|
266
|
+
expect(transport.items[0]?.meta.session?.id).toBe('session-b');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('leaves an iOS crash pending when delivery fails', async () => {
|
|
270
|
+
const setup = setupIosReplay([createCrashReport({ reportId: 'ios-report-a' })]);
|
|
271
|
+
iosInstrumentations.push(setup.instrumentation);
|
|
272
|
+
setup.sendSpy.mockResolvedValue({ outcome: 'failed' });
|
|
273
|
+
|
|
274
|
+
await setup.testable.processCrashReports(setup.nativeModule);
|
|
275
|
+
|
|
276
|
+
expect(setup.sendSpy).toHaveBeenCalledTimes(1);
|
|
277
|
+
expect(setup.nativeModule.acknowledgeCrashReports).not.toHaveBeenCalled();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('discards an iOS crash without assigning the current session when its original session is unavailable', async () => {
|
|
281
|
+
const setup = setupIosReplay([
|
|
282
|
+
createCrashReport({
|
|
283
|
+
reportId: 'ios-report-a',
|
|
284
|
+
sessionId: undefined,
|
|
285
|
+
}),
|
|
286
|
+
]);
|
|
287
|
+
iosInstrumentations.push(setup.instrumentation);
|
|
288
|
+
|
|
289
|
+
await setup.testable.processCrashReports(setup.nativeModule);
|
|
290
|
+
|
|
291
|
+
expect(setup.sendSpy).not.toHaveBeenCalled();
|
|
292
|
+
expect(setup.faro.metas.value.session?.id).toBe('session-b');
|
|
293
|
+
expect(setup.nativeModule.acknowledgeCrashReports).toHaveBeenCalledWith(['ios-report-a']);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('discards an iOS crash older than the replay window', async () => {
|
|
297
|
+
const now = 1_800_000_000_000;
|
|
298
|
+
jest.spyOn(Date, 'now').mockReturnValue(now);
|
|
299
|
+
const setup = setupIosReplay([
|
|
300
|
+
createCrashReport({
|
|
301
|
+
reportId: 'ios-report-a',
|
|
302
|
+
timestamp: now - 7 * 24 * 60 * 60 * 1000 - 1,
|
|
303
|
+
}),
|
|
304
|
+
]);
|
|
305
|
+
iosInstrumentations.push(setup.instrumentation);
|
|
306
|
+
|
|
307
|
+
await setup.testable.processCrashReports(setup.nativeModule);
|
|
308
|
+
|
|
309
|
+
expect(setup.sendSpy).not.toHaveBeenCalled();
|
|
310
|
+
expect(setup.nativeModule.acknowledgeCrashReports).toHaveBeenCalledWith(['ios-report-a']);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('persists later live-session changes without mutating a recovered crash', async () => {
|
|
314
|
+
const setup = setupAndroidReplay([createCrashReport()]);
|
|
315
|
+
androidInstrumentations.push(setup.instrumentation);
|
|
316
|
+
|
|
317
|
+
setup.faro.api.setSession({ id: 'session-c' });
|
|
318
|
+
await setup.testable.processCrashReports(setup.nativeModule);
|
|
319
|
+
|
|
320
|
+
expect(setup.nativeModule.recordCrashSessionContext.mock.calls.map(([context]) => context.sessionId)).toEqual([
|
|
321
|
+
'session-b',
|
|
322
|
+
'session-c',
|
|
323
|
+
]);
|
|
324
|
+
const sentItem = setup.sendSpy.mock.calls[0]?.[0]?.[0];
|
|
325
|
+
expect(sentItem?.meta.session?.id).toBe('session-a');
|
|
326
|
+
expect(setup.faro.metas.value.session?.id).toBe('session-c');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('retries persisting session context until native storage confirms success', () => {
|
|
330
|
+
const setup = setupAndroidReplay([], undefined, false);
|
|
331
|
+
androidInstrumentations.push(setup.instrumentation);
|
|
332
|
+
setup.nativeModule.recordCrashSessionContext.mockReturnValue(undefined);
|
|
333
|
+
|
|
334
|
+
setup.testable.prepareCrashReporting(setup.nativeModule);
|
|
335
|
+
setup.testable.prepareCrashReporting(setup.nativeModule);
|
|
336
|
+
|
|
337
|
+
expect(setup.nativeModule.recordCrashSessionContext).toHaveBeenCalledTimes(3);
|
|
338
|
+
expect(setup.nativeModule.recordCrashSessionContext).toHaveBeenNthCalledWith(
|
|
339
|
+
3,
|
|
340
|
+
expect.objectContaining({ sessionId: 'session-b' })
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it.each(['failed', 'rejected', 'skipped'] as const)(
|
|
345
|
+
'leaves the report pending when delivery is %s',
|
|
346
|
+
async (outcome) => {
|
|
347
|
+
const setup = setupAndroidReplay([createCrashReport()]);
|
|
348
|
+
androidInstrumentations.push(setup.instrumentation);
|
|
349
|
+
setup.sendSpy.mockResolvedValue(
|
|
350
|
+
outcome === 'rejected'
|
|
351
|
+
? { outcome, status: 500 }
|
|
352
|
+
: {
|
|
353
|
+
outcome,
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
await setup.testable.processCrashReports(setup.nativeModule);
|
|
358
|
+
|
|
359
|
+
expect(setup.sendSpy).toHaveBeenCalledTimes(1);
|
|
360
|
+
expect(setup.nativeModule.acknowledgeCrashReports).not.toHaveBeenCalled();
|
|
361
|
+
}
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
it('replays multiple historical crashes under their own sessions and de-duplicates report IDs', async () => {
|
|
365
|
+
const setup = setupAndroidReplay([
|
|
366
|
+
createCrashReport({ reportId: 'report-a', sessionId: 'session-a', timestamp: Date.now() - 2000 }),
|
|
367
|
+
createCrashReport({ reportId: 'report-b', sessionId: 'session-b-old', timestamp: Date.now() - 1000 }),
|
|
368
|
+
createCrashReport({ reportId: 'report-a', sessionId: 'session-a', timestamp: Date.now() - 2000 }),
|
|
369
|
+
]);
|
|
370
|
+
androidInstrumentations.push(setup.instrumentation);
|
|
371
|
+
|
|
372
|
+
await setup.testable.processCrashReports(setup.nativeModule);
|
|
373
|
+
|
|
374
|
+
expect(setup.sendSpy).toHaveBeenCalledTimes(2);
|
|
375
|
+
expect(setup.sendSpy.mock.calls.map(([items]) => items[0]?.meta.session?.id)).toEqual([
|
|
376
|
+
'session-a',
|
|
377
|
+
'session-b-old',
|
|
378
|
+
]);
|
|
379
|
+
expect(setup.nativeModule.acknowledgeCrashReports).toHaveBeenCalledTimes(2);
|
|
380
|
+
expect(setup.nativeModule.acknowledgeCrashReports).toHaveBeenNthCalledWith(1, ['report-a']);
|
|
381
|
+
expect(setup.nativeModule.acknowledgeCrashReports).toHaveBeenNthCalledWith(2, ['report-b']);
|
|
382
|
+
expect(setup.faro.metas.value.session?.id).toBe('session-b');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('accepts the seven-day boundary and acknowledges an older report without sending it', async () => {
|
|
386
|
+
const now = 1_800_000_000_000;
|
|
387
|
+
jest.spyOn(Date, 'now').mockReturnValue(now);
|
|
388
|
+
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
|
|
389
|
+
const setup = setupAndroidReplay([
|
|
390
|
+
createCrashReport({ reportId: 'boundary', timestamp: now - sevenDaysMs }),
|
|
391
|
+
createCrashReport({ reportId: 'expired', timestamp: now - sevenDaysMs - 1 }),
|
|
392
|
+
]);
|
|
393
|
+
androidInstrumentations.push(setup.instrumentation);
|
|
394
|
+
|
|
395
|
+
await setup.testable.processCrashReports(setup.nativeModule);
|
|
396
|
+
|
|
397
|
+
expect(setup.sendSpy).toHaveBeenCalledTimes(1);
|
|
398
|
+
expect(setup.nativeModule.acknowledgeCrashReports).toHaveBeenCalledWith(['boundary']);
|
|
399
|
+
expect(setup.nativeModule.acknowledgeCrashReports).toHaveBeenCalledWith(['expired']);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('acknowledges a report without sending it when the original session context is missing', async () => {
|
|
403
|
+
const setup = setupAndroidReplay([createCrashReport({ sessionId: undefined })]);
|
|
404
|
+
androidInstrumentations.push(setup.instrumentation);
|
|
405
|
+
|
|
406
|
+
await setup.testable.processCrashReports(setup.nativeModule);
|
|
407
|
+
|
|
408
|
+
expect(setup.sendSpy).not.toHaveBeenCalled();
|
|
409
|
+
expect(setup.nativeModule.acknowledgeCrashReports).toHaveBeenCalledWith(['report-a']);
|
|
410
|
+
expect(setup.faro.metas.value.session?.id).toBe('session-b');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('acknowledges only the malformed report when its stable ID is available', async () => {
|
|
414
|
+
const setup = setupAndroidReplay([
|
|
415
|
+
createCrashReport({ reportId: 'malformed', timestamp: Number.NaN }),
|
|
416
|
+
createCrashReport({ reportId: 'valid' }),
|
|
417
|
+
]);
|
|
418
|
+
androidInstrumentations.push(setup.instrumentation);
|
|
419
|
+
|
|
420
|
+
await setup.testable.processCrashReports(setup.nativeModule);
|
|
421
|
+
|
|
422
|
+
expect(setup.sendSpy).toHaveBeenCalledTimes(1);
|
|
423
|
+
expect(setup.nativeModule.acknowledgeCrashReports).toHaveBeenCalledWith(['malformed']);
|
|
424
|
+
expect(setup.nativeModule.acknowledgeCrashReports).toHaveBeenCalledWith(['valid']);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('acknowledges a platform-filtered report without sending it repeatedly', async () => {
|
|
428
|
+
const setup = setupAndroidReplay([
|
|
429
|
+
createCrashReport({
|
|
430
|
+
description: 'Input dispatching timed out',
|
|
431
|
+
reportId: 'anr-report',
|
|
432
|
+
}),
|
|
433
|
+
]);
|
|
434
|
+
androidInstrumentations.push(setup.instrumentation);
|
|
435
|
+
|
|
436
|
+
await setup.testable.processCrashReports(setup.nativeModule);
|
|
437
|
+
|
|
438
|
+
expect(setup.sendSpy).not.toHaveBeenCalled();
|
|
439
|
+
expect(setup.nativeModule.acknowledgeCrashReports).toHaveBeenCalledWith(['anr-report']);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('honors beforeSend filtering and acknowledges the handled report', async () => {
|
|
443
|
+
const beforeSend = jest.fn(() => null);
|
|
444
|
+
const setup = setupAndroidReplay([createCrashReport()], beforeSend);
|
|
445
|
+
androidInstrumentations.push(setup.instrumentation);
|
|
446
|
+
|
|
447
|
+
await setup.testable.processCrashReports(setup.nativeModule);
|
|
448
|
+
|
|
449
|
+
expect(beforeSend).toHaveBeenCalledWith(
|
|
450
|
+
expect.objectContaining({
|
|
451
|
+
meta: expect.objectContaining({
|
|
452
|
+
session: expect.objectContaining({ id: 'session-a' }),
|
|
453
|
+
}),
|
|
454
|
+
})
|
|
455
|
+
);
|
|
456
|
+
expect(setup.sendSpy).not.toHaveBeenCalled();
|
|
457
|
+
expect(setup.nativeModule.acknowledgeCrashReports).toHaveBeenCalledWith(['report-a']);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('treats an invalid undefined beforeSend result as filtered', async () => {
|
|
461
|
+
const beforeSend = jest.fn(() => undefined) as unknown as (item: TransportItem) => TransportItem | null;
|
|
462
|
+
const setup = setupAndroidReplay([createCrashReport()], beforeSend);
|
|
463
|
+
androidInstrumentations.push(setup.instrumentation);
|
|
464
|
+
|
|
465
|
+
await setup.testable.processCrashReports(setup.nativeModule);
|
|
466
|
+
|
|
467
|
+
expect(setup.sendSpy).not.toHaveBeenCalled();
|
|
468
|
+
expect(setup.nativeModule.acknowledgeCrashReports).toHaveBeenCalledWith(['report-a']);
|
|
469
|
+
});
|
|
42
470
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { VERSION } from '@grafana/faro-core';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { RecoveredCrashReportingInstrumentation } from '../RecoveredCrashReportingInstrumentation';
|
|
4
4
|
import type { CrashReport } from '../types';
|
|
5
5
|
|
|
6
6
|
import { resolveCrashErrorMessage, shouldSkipCrashReport } from './crashErrorMessage';
|
|
@@ -20,7 +20,7 @@ import { type ParsedIosCrashTrace, parseIosCrashTrace } from './parseIosCrashTra
|
|
|
20
20
|
*
|
|
21
21
|
* Stack frames are symbolicated on the native side before being surfaced.
|
|
22
22
|
*/
|
|
23
|
-
export class IosCrashReportingInstrumentation extends
|
|
23
|
+
export class IosCrashReportingInstrumentation extends RecoveredCrashReportingInstrumentation {
|
|
24
24
|
readonly name = '@grafana/faro-react-native:instrumentation-crash-ios';
|
|
25
25
|
readonly version = VERSION;
|
|
26
26
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Crash report data structure returned from native module.
|
|
3
3
|
*/
|
|
4
4
|
export interface CrashReport {
|
|
5
|
+
reportId?: string;
|
|
5
6
|
trace?: string;
|
|
6
7
|
signal?: string;
|
|
7
8
|
timestamp?: number;
|
|
@@ -12,6 +13,12 @@ export interface CrashReport {
|
|
|
12
13
|
// iOS-specific fields
|
|
13
14
|
reason?: string; // Signal name (e.g., 'SIGSEGV')
|
|
14
15
|
status?: number; // Exit status code
|
|
16
|
+
// Recovered-crash session context
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
isSampled?: boolean;
|
|
19
|
+
appVersion?: string;
|
|
20
|
+
appRelease?: string;
|
|
21
|
+
appBundleId?: string;
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
/**
|
|
@@ -14,7 +14,9 @@ declare const global: {
|
|
|
14
14
|
removeEventListener?: (event: string, handler: (event: PromiseRejectionEvent) => void) => void;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// Mirrors RN's ErrorHandler: a thrown value is not necessarily an Error, and
|
|
18
|
+
// isFatal is always supplied by the runtime.
|
|
19
|
+
type ErrorHandlerCallback = (error: unknown, isFatal: boolean) => void;
|
|
18
20
|
|
|
19
21
|
export interface ErrorsInstrumentationOptions {
|
|
20
22
|
/**
|
|
@@ -115,8 +117,14 @@ export class ErrorsInstrumentation extends BaseInstrumentation {
|
|
|
115
117
|
this.originalErrorHandler = global.ErrorUtils.getGlobalHandler();
|
|
116
118
|
|
|
117
119
|
// Set our custom handler
|
|
118
|
-
global.ErrorUtils.setGlobalHandler((
|
|
120
|
+
global.ErrorUtils.setGlobalHandler((thrown: unknown, isFatal: boolean) => {
|
|
119
121
|
try {
|
|
122
|
+
// JS can throw any value, so normalise before the Error-shaped paths
|
|
123
|
+
// below. String() can itself throw — on a null-prototype object, or one
|
|
124
|
+
// with a throwing toString — so keep it inside the try, or finally never
|
|
125
|
+
// runs and React Native's own handler never sees the crash.
|
|
126
|
+
const error = thrown instanceof Error ? thrown : new Error(String(thrown));
|
|
127
|
+
|
|
120
128
|
// Check if error should be ignored
|
|
121
129
|
if (this.shouldIgnoreError(error)) {
|
|
122
130
|
return;
|
|
@@ -144,7 +152,7 @@ export class ErrorsInstrumentation extends BaseInstrumentation {
|
|
|
144
152
|
this.api.pushError(enhancedError, {
|
|
145
153
|
type: enhancedError.name || 'Error',
|
|
146
154
|
context,
|
|
147
|
-
fatal: isFatal
|
|
155
|
+
fatal: isFatal,
|
|
148
156
|
stackFrames,
|
|
149
157
|
});
|
|
150
158
|
|
|
@@ -157,7 +165,7 @@ export class ErrorsInstrumentation extends BaseInstrumentation {
|
|
|
157
165
|
} finally {
|
|
158
166
|
// Always call the original handler to maintain normal error behavior
|
|
159
167
|
if (this.originalErrorHandler) {
|
|
160
|
-
this.originalErrorHandler(
|
|
168
|
+
this.originalErrorHandler(thrown, isFatal);
|
|
161
169
|
}
|
|
162
170
|
}
|
|
163
171
|
});
|
|
@@ -577,5 +577,40 @@ describe('ErrorsInstrumentation', () => {
|
|
|
577
577
|
expect(() => errorHandler(errorWithoutStack, false)).not.toThrow();
|
|
578
578
|
expect(transport.items).toHaveLength(1);
|
|
579
579
|
});
|
|
580
|
+
|
|
581
|
+
it.each([
|
|
582
|
+
['a null-prototype object', () => Object.create(null)],
|
|
583
|
+
[
|
|
584
|
+
'an object with a throwing toString',
|
|
585
|
+
() => ({
|
|
586
|
+
toString: () => {
|
|
587
|
+
throw new Error('toString exploded');
|
|
588
|
+
},
|
|
589
|
+
}),
|
|
590
|
+
],
|
|
591
|
+
])('should still forward %s to the original handler', (_name, makeThrown) => {
|
|
592
|
+
const originalHandler = jest.fn();
|
|
593
|
+
mockGlobal.ErrorUtils.getGlobalHandler = jest.fn(() => originalHandler);
|
|
594
|
+
|
|
595
|
+
let errorHandler: any;
|
|
596
|
+
mockGlobal.ErrorUtils.setGlobalHandler = jest.fn((handler) => {
|
|
597
|
+
errorHandler = handler;
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
const transport = new MockTransport();
|
|
601
|
+
initializeFaro(
|
|
602
|
+
mockConfig({
|
|
603
|
+
transports: [transport],
|
|
604
|
+
instrumentations: [new ErrorsInstrumentation()],
|
|
605
|
+
})
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
// String(thrown) throws for these values, so Faro cannot report them.
|
|
609
|
+
// React Native's handler must still see the original value and flag.
|
|
610
|
+
const thrown = makeThrown();
|
|
611
|
+
expect(() => errorHandler(thrown, true)).not.toThrow();
|
|
612
|
+
expect(originalHandler).toHaveBeenCalledWith(thrown, true);
|
|
613
|
+
expect(transport.items).toHaveLength(0);
|
|
614
|
+
});
|
|
580
615
|
});
|
|
581
616
|
});
|