@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.
Files changed (266) hide show
  1. package/README.md +176 -30
  2. package/android/build.gradle +20 -15
  3. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashIds.kt +44 -0
  4. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashReporter.kt +134 -38
  5. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashSessionStore.kt +433 -0
  6. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashTraceCache.kt +11 -2
  7. package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativeModule.kt +128 -17
  8. package/android/src/main/java/com/grafana/faro/reactnative/FaroSessionProcess.kt +88 -0
  9. package/android/src/main/java/com/grafana/faro/reactnative/FrameMonitor.kt +34 -42
  10. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterBelowApi30Test.kt +21 -0
  11. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashReporterTest.kt +59 -0
  12. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashSessionStoreTest.kt +268 -0
  13. package/android/src/test/java/com/grafana/faro/reactnative/FaroCrashTraceCacheTest.kt +52 -0
  14. package/android/src/test/java/com/grafana/faro/reactnative/FaroSessionProcessTest.kt +127 -0
  15. package/dist/cjs/config/getRNInstrumentations.js +6 -8
  16. package/dist/cjs/config/getRNInstrumentations.js.map +1 -1
  17. package/dist/cjs/config/sampling.js.map +1 -1
  18. package/dist/cjs/config/types.js.map +1 -1
  19. package/dist/cjs/dataCollection/DataCollectionPolicy.js +1 -2
  20. package/dist/cjs/dataCollection/DataCollectionPolicy.js.map +1 -1
  21. package/dist/cjs/generated/faroRNPackageMeta.js +1 -1
  22. package/dist/cjs/generated/faroRNPackageMeta.js.map +1 -1
  23. package/dist/cjs/index.js +8 -3
  24. package/dist/cjs/index.js.map +1 -1
  25. package/dist/cjs/instrumentations/anr/instrumentation.js +10 -1
  26. package/dist/cjs/instrumentations/anr/instrumentation.js.map +1 -1
  27. package/dist/cjs/instrumentations/appState/index.js +8 -2
  28. package/dist/cjs/instrumentations/appState/index.js.map +1 -1
  29. package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +19 -6
  30. package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
  31. package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +331 -0
  32. package/dist/cjs/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
  33. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
  34. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
  35. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
  36. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
  37. package/dist/cjs/instrumentations/crashReporting/types.js.map +1 -1
  38. package/dist/cjs/instrumentations/errors/index.js +8 -3
  39. package/dist/cjs/instrumentations/errors/index.js.map +1 -1
  40. package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js +20 -31
  41. package/dist/cjs/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
  42. package/dist/cjs/instrumentations/frameMonitoring/types.js.map +1 -1
  43. package/dist/cjs/instrumentations/performance/index.js +2 -6
  44. package/dist/cjs/instrumentations/performance/index.js.map +1 -1
  45. package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js +48 -0
  46. package/dist/cjs/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
  47. package/dist/cjs/instrumentations/session/directSessionActivity.js +37 -0
  48. package/dist/cjs/instrumentations/session/directSessionActivity.js.map +1 -0
  49. package/dist/cjs/instrumentations/session/index.js +214 -45
  50. package/dist/cjs/instrumentations/session/index.js.map +1 -1
  51. package/dist/cjs/instrumentations/session/sessionActivity.js +43 -0
  52. package/dist/cjs/instrumentations/session/sessionActivity.js.map +1 -0
  53. package/dist/cjs/instrumentations/session/sessionAttributes.js +8 -8
  54. package/dist/cjs/instrumentations/session/sessionAttributes.js.map +1 -1
  55. package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +199 -24
  56. package/dist/cjs/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
  57. package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -13
  58. package/dist/cjs/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
  59. package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js +72 -0
  60. package/dist/cjs/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
  61. package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js +1 -1
  62. package/dist/cjs/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
  63. package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js +124 -96
  64. package/dist/cjs/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
  65. package/dist/cjs/instrumentations/session/sessionManager/types.js.map +1 -1
  66. package/dist/cjs/instrumentations/session/sessionProcess.js +96 -0
  67. package/dist/cjs/instrumentations/session/sessionProcess.js.map +1 -0
  68. package/dist/cjs/instrumentations/startup/index.js +2 -6
  69. package/dist/cjs/instrumentations/startup/index.js.map +1 -1
  70. package/dist/cjs/navigation/utils.js +3 -1
  71. package/dist/cjs/navigation/utils.js.map +1 -1
  72. package/dist/cjs/transports/fetch/index.js.map +1 -1
  73. package/dist/cjs/transports/fetch/transport.js +56 -25
  74. package/dist/cjs/transports/fetch/transport.js.map +1 -1
  75. package/dist/cjs/transports/fetch/types.js.map +1 -1
  76. package/dist/cjs/transports/offline/ConnectivityService.js +1 -1
  77. package/dist/cjs/transports/offline/ConnectivityService.js.map +1 -1
  78. package/dist/cjs/transports/offline/OfflineCache.js +2 -2
  79. package/dist/cjs/transports/offline/OfflineCache.js.map +1 -1
  80. package/dist/cjs/transports/offline/transport.js +3 -4
  81. package/dist/cjs/transports/offline/transport.js.map +1 -1
  82. package/dist/cjs/userPersistence/UserPersistence.js +1 -2
  83. package/dist/cjs/userPersistence/UserPersistence.js.map +1 -1
  84. package/dist/esm/config/getRNInstrumentations.js +6 -8
  85. package/dist/esm/config/getRNInstrumentations.js.map +1 -1
  86. package/dist/esm/config/sampling.js.map +1 -1
  87. package/dist/esm/config/types.js.map +1 -1
  88. package/dist/esm/dataCollection/DataCollectionPolicy.js +1 -2
  89. package/dist/esm/dataCollection/DataCollectionPolicy.js.map +1 -1
  90. package/dist/esm/generated/faroRNPackageMeta.js +1 -1
  91. package/dist/esm/generated/faroRNPackageMeta.js.map +1 -1
  92. package/dist/esm/index.js +4 -2
  93. package/dist/esm/index.js.map +1 -1
  94. package/dist/esm/instrumentations/anr/instrumentation.js +6 -1
  95. package/dist/esm/instrumentations/anr/instrumentation.js.map +1 -1
  96. package/dist/esm/instrumentations/appState/index.js +8 -2
  97. package/dist/esm/instrumentations/appState/index.js.map +1 -1
  98. package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js +15 -4
  99. package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -1
  100. package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js +230 -0
  101. package/dist/esm/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.js.map +1 -0
  102. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +2 -2
  103. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -1
  104. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +2 -2
  105. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -1
  106. package/dist/esm/instrumentations/crashReporting/types.js.map +1 -1
  107. package/dist/esm/instrumentations/errors/index.js +8 -3
  108. package/dist/esm/instrumentations/errors/index.js.map +1 -1
  109. package/dist/esm/instrumentations/frameMonitoring/instrumentation.js +17 -32
  110. package/dist/esm/instrumentations/frameMonitoring/instrumentation.js.map +1 -1
  111. package/dist/esm/instrumentations/frameMonitoring/types.js.map +1 -1
  112. package/dist/esm/instrumentations/performance/index.js +2 -6
  113. package/dist/esm/instrumentations/performance/index.js.map +1 -1
  114. package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js +34 -0
  115. package/dist/esm/instrumentations/session/FaroSessionActivityBoundary.js.map +1 -0
  116. package/dist/esm/instrumentations/session/directSessionActivity.js +32 -0
  117. package/dist/esm/instrumentations/session/directSessionActivity.js.map +1 -0
  118. package/dist/esm/instrumentations/session/index.js +207 -46
  119. package/dist/esm/instrumentations/session/index.js.map +1 -1
  120. package/dist/esm/instrumentations/session/sessionActivity.js +38 -0
  121. package/dist/esm/instrumentations/session/sessionActivity.js.map +1 -0
  122. package/dist/esm/instrumentations/session/sessionAttributes.js +7 -7
  123. package/dist/esm/instrumentations/session/sessionAttributes.js.map +1 -1
  124. package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js +190 -27
  125. package/dist/esm/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.js.map +1 -1
  126. package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js +31 -14
  127. package/dist/esm/instrumentations/session/sessionManager/VolatileSessionManager.js.map +1 -1
  128. package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js +56 -0
  129. package/dist/esm/instrumentations/session/sessionManager/persistentSessionRecord.js.map +1 -0
  130. package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js +1 -1
  131. package/dist/esm/instrumentations/session/sessionManager/sessionConstants.js.map +1 -1
  132. package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js +120 -44
  133. package/dist/esm/instrumentations/session/sessionManager/sessionManagerUtils.js.map +1 -1
  134. package/dist/esm/instrumentations/session/sessionManager/types.js.map +1 -1
  135. package/dist/esm/instrumentations/session/sessionProcess.js +89 -0
  136. package/dist/esm/instrumentations/session/sessionProcess.js.map +1 -0
  137. package/dist/esm/instrumentations/startup/index.js +2 -6
  138. package/dist/esm/instrumentations/startup/index.js.map +1 -1
  139. package/dist/esm/navigation/utils.js +2 -1
  140. package/dist/esm/navigation/utils.js.map +1 -1
  141. package/dist/esm/transports/fetch/index.js.map +1 -1
  142. package/dist/esm/transports/fetch/transport.js +38 -14
  143. package/dist/esm/transports/fetch/transport.js.map +1 -1
  144. package/dist/esm/transports/fetch/types.js.map +1 -1
  145. package/dist/esm/transports/offline/ConnectivityService.js +1 -1
  146. package/dist/esm/transports/offline/ConnectivityService.js.map +1 -1
  147. package/dist/esm/transports/offline/OfflineCache.js +2 -2
  148. package/dist/esm/transports/offline/OfflineCache.js.map +1 -1
  149. package/dist/esm/transports/offline/transport.js +3 -4
  150. package/dist/esm/transports/offline/transport.js.map +1 -1
  151. package/dist/esm/userPersistence/UserPersistence.js +1 -2
  152. package/dist/esm/userPersistence/UserPersistence.js.map +1 -1
  153. package/dist/types/config/getRNInstrumentations.d.ts +1 -2
  154. package/dist/types/config/sampling.d.ts +0 -3
  155. package/dist/types/config/types.d.ts +5 -3
  156. package/dist/types/dataCollection/DataCollectionPolicy.d.ts +1 -2
  157. package/dist/types/generated/faroRNPackageMeta.d.ts +1 -1
  158. package/dist/types/index.d.ts +4 -1
  159. package/dist/types/instrumentations/anr/instrumentation.d.ts +1 -1
  160. package/dist/types/instrumentations/appState/index.d.ts +1 -1
  161. package/dist/types/instrumentations/console/index.d.ts +1 -1
  162. package/dist/types/instrumentations/crashReporting/BaseCrashReportingInstrumentation.d.ts +14 -4
  163. package/dist/types/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.d.ts +1 -1
  164. package/dist/types/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.d.ts +27 -0
  165. package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +3 -3
  166. package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +3 -3
  167. package/dist/types/instrumentations/crashReporting/types.d.ts +6 -0
  168. package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +3 -3
  169. package/dist/types/instrumentations/frameMonitoring/types.d.ts +4 -6
  170. package/dist/types/instrumentations/http/index.d.ts +1 -1
  171. package/dist/types/instrumentations/performance/index.d.ts +1 -3
  172. package/dist/types/instrumentations/session/FaroSessionActivityBoundary.d.ts +8 -0
  173. package/dist/types/instrumentations/session/directSessionActivity.d.ts +13 -0
  174. package/dist/types/instrumentations/session/index.d.ts +21 -2
  175. package/dist/types/instrumentations/session/sessionActivity.d.ts +9 -0
  176. package/dist/types/instrumentations/session/sessionAttributes.d.ts +6 -3
  177. package/dist/types/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.d.ts +16 -3
  178. package/dist/types/instrumentations/session/sessionManager/VolatileSessionManager.d.ts +9 -3
  179. package/dist/types/instrumentations/session/sessionManager/persistentSessionRecord.d.ts +4 -0
  180. package/dist/types/instrumentations/session/sessionManager/sessionManagerUtils.d.ts +19 -7
  181. package/dist/types/instrumentations/session/sessionManager/types.d.ts +6 -0
  182. package/dist/types/instrumentations/session/sessionProcess.d.ts +17 -0
  183. package/dist/types/instrumentations/startup/index.d.ts +3 -7
  184. package/dist/types/instrumentations/userActions/index.d.ts +1 -1
  185. package/dist/types/instrumentations/view/index.d.ts +1 -1
  186. package/dist/types/instrumentations/xhr/index.d.ts +1 -1
  187. package/dist/types/navigation/utils.d.ts +1 -0
  188. package/dist/types/testUtils/mockFaroReactNativeTracing.d.ts +1 -1
  189. package/dist/types/transports/console/transport.d.ts +1 -1
  190. package/dist/types/transports/fetch/index.d.ts +1 -1
  191. package/dist/types/transports/fetch/transport.d.ts +9 -2
  192. package/dist/types/transports/fetch/types.d.ts +5 -0
  193. package/dist/types/transports/offline/ConnectivityService.d.ts +1 -1
  194. package/dist/types/transports/offline/OfflineCache.d.ts +2 -2
  195. package/dist/types/transports/offline/transport.d.ts +4 -5
  196. package/dist/types/userPersistence/UserPersistence.d.ts +1 -2
  197. package/ios/FaroCrashReporter.swift +214 -21
  198. package/ios/FaroReactNative.swift +24 -0
  199. package/ios/FaroReactNativeModule.h +2 -1
  200. package/ios/FaroReactNativeModule.mm +121 -6
  201. package/ios/RefreshRateVitals.swift +34 -38
  202. package/package.json +8 -6
  203. package/src/config/getRNInstrumentations.ts +6 -8
  204. package/src/config/makeRNConfig.test.ts +6 -0
  205. package/src/config/sampling.ts +0 -3
  206. package/src/config/types.ts +5 -3
  207. package/src/dataCollection/DataCollectionPolicy.ts +1 -2
  208. package/src/generated/faroRNPackageMeta.ts +1 -1
  209. package/src/index.ts +8 -2
  210. package/src/instrumentations/anr/instrumentation.ts +8 -1
  211. package/src/instrumentations/appState/index.ts +10 -2
  212. package/src/instrumentations/crashReporting/BaseCrashReportingInstrumentation.ts +28 -8
  213. package/src/instrumentations/crashReporting/RecoveredCrashReportingInstrumentation.ts +277 -0
  214. package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +2 -2
  215. package/src/instrumentations/crashReporting/instrumentation.test.ts +433 -5
  216. package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +2 -2
  217. package/src/instrumentations/crashReporting/types.ts +7 -0
  218. package/src/instrumentations/errors/index.ts +12 -4
  219. package/src/instrumentations/errors/instrumentation.test.ts +35 -0
  220. package/src/instrumentations/frameMonitoring/instrumentation.test.ts +155 -0
  221. package/src/instrumentations/frameMonitoring/instrumentation.ts +19 -38
  222. package/src/instrumentations/frameMonitoring/types.ts +4 -6
  223. package/src/instrumentations/performance/index.ts +2 -6
  224. package/src/instrumentations/session/FaroSessionActivityBoundary.test.ts +60 -0
  225. package/src/instrumentations/session/FaroSessionActivityBoundary.tsx +41 -0
  226. package/src/instrumentations/session/beforeSendHook.test.ts +246 -0
  227. package/src/instrumentations/session/directSessionActivity.test.ts +158 -0
  228. package/src/instrumentations/session/directSessionActivity.ts +36 -0
  229. package/src/instrumentations/session/index.ts +269 -46
  230. package/src/instrumentations/session/persistentColdStart.test.ts +196 -0
  231. package/src/instrumentations/session/sessionActivity.test.ts +68 -0
  232. package/src/instrumentations/session/sessionActivity.ts +58 -0
  233. package/src/instrumentations/session/sessionAttributes.test.ts +37 -2
  234. package/src/instrumentations/session/sessionAttributes.ts +12 -6
  235. package/src/instrumentations/session/sessionCleanup.test.ts +410 -0
  236. package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.test.ts +552 -0
  237. package/src/instrumentations/session/sessionManager/MmkvPersistentSessionsManager.ts +249 -33
  238. package/src/instrumentations/session/sessionManager/VolatileSessionManager.ts +38 -18
  239. package/src/instrumentations/session/sessionManager/persistentSessionRecord.test.ts +103 -0
  240. package/src/instrumentations/session/sessionManager/persistentSessionRecord.ts +78 -0
  241. package/src/instrumentations/session/sessionManager/sampling.test.ts +1 -1
  242. package/src/instrumentations/session/sessionManager/sessionConstants.ts +1 -1
  243. package/src/instrumentations/session/sessionManager/sessionManagerUtils.test.ts +256 -18
  244. package/src/instrumentations/session/sessionManager/sessionManagerUtils.ts +180 -39
  245. package/src/instrumentations/session/sessionManager/types.ts +8 -0
  246. package/src/instrumentations/session/sessionManagerInstance.test.ts +65 -0
  247. package/src/instrumentations/session/sessionProcess.test.ts +136 -0
  248. package/src/instrumentations/session/sessionProcess.ts +114 -0
  249. package/src/instrumentations/session/startNewSession.test.ts +337 -0
  250. package/src/instrumentations/startup/index.ts +2 -6
  251. package/src/navigation/utils.ts +3 -1
  252. package/src/transports/fetch/index.ts +6 -1
  253. package/src/transports/fetch/transport.test.ts +86 -2
  254. package/src/transports/fetch/transport.ts +38 -16
  255. package/src/transports/fetch/types.ts +7 -0
  256. package/src/transports/offline/ConnectivityService.ts +1 -1
  257. package/src/transports/offline/OfflineCache.ts +2 -2
  258. package/src/transports/offline/transport.ts +3 -4
  259. package/src/userPersistence/UserPersistence.ts +1 -2
  260. package/dist/cjs/utils/throttle.js +0 -42
  261. package/dist/cjs/utils/throttle.js.map +0 -1
  262. package/dist/esm/utils/throttle.js +0 -34
  263. package/dist/esm/utils/throttle.js.map +0 -1
  264. package/dist/types/utils/throttle.d.ts +0 -9
  265. package/src/utils/throttle.test.ts +0 -141
  266. 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 { BaseCrashReportingInstrumentation } from '../BaseCrashReportingInstrumentation';
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 BaseCrashReportingInstrumentation {
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
- type ErrorHandlerCallback = (error: Error | unknown, isFatal?: boolean) => void;
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((error: Error, isFatal?: boolean) => {
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 ?? false,
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(error, isFatal);
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
  });