@grafana/faro-react-native 1.2.1 → 1.3.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.
Files changed (217) hide show
  1. package/android/build.gradle +118 -36
  2. package/android/consumer-proguard-rules.pro +2 -0
  3. package/android/src/main/java/com/grafana/faro/reactnative/ANRTracker.kt +61 -9
  4. package/android/src/main/java/com/grafana/faro/reactnative/AnrThreadDumpParser.kt +70 -0
  5. package/android/src/main/java/com/grafana/faro/reactnative/ApplicationExitTraceReader.kt +142 -0
  6. package/android/src/main/java/com/grafana/faro/reactnative/FaroAnrCache.kt +163 -0
  7. package/android/src/main/java/com/grafana/faro/reactnative/FaroAnrReporter.kt +163 -0
  8. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashReporter.kt +243 -50
  9. package/android/src/main/java/com/grafana/faro/reactnative/FaroCrashTraceCache.kt +75 -0
  10. package/android/src/main/java/com/grafana/faro/reactnative/FaroNativeCrashTrace.kt +39 -0
  11. package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativeModule.kt +99 -8
  12. package/android/src/main/java/com/grafana/faro/reactnative/FaroReactNativePackage.kt +1 -0
  13. package/android/src/main/java/com/grafana/faro/reactnative/FaroUncaughtExceptionHandler.kt +62 -0
  14. package/android/src/main/java/com/grafana/faro/reactnative/TombstoneBacktraceFormatter.kt +117 -0
  15. package/android/src/main/java/com/grafana/faro/reactnative/TombstoneModels.kt +35 -0
  16. package/android/src/main/java/com/grafana/faro/reactnative/TombstoneWireParser.kt +235 -0
  17. package/android/src/main/proto/tombstone.proto +62 -0
  18. package/android/src/test/java/com/grafana/faro/reactnative/AnrStackFiltersTest.kt +28 -0
  19. package/android/src/test/java/com/grafana/faro/reactnative/AnrThreadDumpParserTest.kt +38 -0
  20. package/android/src/test/java/com/grafana/faro/reactnative/FaroAnrCacheTest.kt +81 -0
  21. package/android/src/test/java/com/grafana/faro/reactnative/TombstoneWireParserTest.kt +51 -0
  22. package/dist/cjs/config/getRNInstrumentations.js +3 -1
  23. package/dist/cjs/config/getRNInstrumentations.js.map +1 -1
  24. package/dist/cjs/config/makeRNConfig.js +48 -9
  25. package/dist/cjs/config/makeRNConfig.js.map +1 -1
  26. package/dist/cjs/config/types.js.map +1 -1
  27. package/dist/cjs/generated/faroRNPackageMeta.js +1 -1
  28. package/dist/cjs/generated/faroRNPackageMeta.js.map +1 -1
  29. package/dist/cjs/index.js +3 -2
  30. package/dist/cjs/index.js.map +1 -1
  31. package/dist/cjs/initialize.js +14 -8
  32. package/dist/cjs/initialize.js.map +1 -1
  33. package/dist/cjs/instrumentations/anr/anrStack.js +19 -0
  34. package/dist/cjs/instrumentations/anr/anrStack.js.map +1 -0
  35. package/dist/cjs/instrumentations/anr/instrumentation.js +374 -72
  36. package/dist/cjs/instrumentations/anr/instrumentation.js.map +1 -1
  37. package/dist/cjs/instrumentations/anr/types.js.map +1 -1
  38. package/dist/cjs/instrumentations/console/index.js +5 -1
  39. package/dist/cjs/instrumentations/console/index.js.map +1 -1
  40. package/dist/cjs/instrumentations/crashReporting/{instrumentation.js → BaseCrashReportingInstrumentation.js} +110 -103
  41. package/dist/cjs/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -0
  42. package/dist/cjs/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.js +41 -0
  43. package/dist/cjs/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.js.map +1 -0
  44. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +87 -0
  45. package/dist/cjs/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -0
  46. package/dist/cjs/instrumentations/crashReporting/android/crashErrorMessage.js +123 -0
  47. package/dist/cjs/instrumentations/crashReporting/android/crashErrorMessage.js.map +1 -0
  48. package/dist/cjs/instrumentations/crashReporting/android/index.js +12 -0
  49. package/dist/cjs/instrumentations/crashReporting/android/index.js.map +1 -0
  50. package/dist/cjs/instrumentations/crashReporting/android/nativeTombstoneTrace.js +11 -0
  51. package/dist/cjs/instrumentations/crashReporting/android/nativeTombstoneTrace.js.map +1 -0
  52. package/dist/cjs/instrumentations/crashReporting/android/parseAndroidCrashTrace.js +181 -0
  53. package/dist/cjs/instrumentations/crashReporting/android/parseAndroidCrashTrace.js.map +1 -0
  54. package/dist/cjs/instrumentations/crashReporting/index.js +35 -2
  55. package/dist/cjs/instrumentations/crashReporting/index.js.map +1 -1
  56. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +78 -0
  57. package/dist/cjs/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -0
  58. package/dist/cjs/instrumentations/crashReporting/ios/crashErrorMessage.js +103 -0
  59. package/dist/cjs/instrumentations/crashReporting/ios/crashErrorMessage.js.map +1 -0
  60. package/dist/cjs/instrumentations/crashReporting/ios/index.js +11 -0
  61. package/dist/cjs/instrumentations/crashReporting/ios/index.js.map +1 -0
  62. package/dist/cjs/instrumentations/crashReporting/ios/parseIosCrashTrace.js +48 -0
  63. package/dist/cjs/instrumentations/crashReporting/ios/parseIosCrashTrace.js.map +1 -0
  64. package/dist/cjs/instrumentations/crashReporting/types.js.map +1 -1
  65. package/dist/cjs/instrumentations/errors/index.js +2 -1
  66. package/dist/cjs/instrumentations/errors/index.js.map +1 -1
  67. package/dist/cjs/instrumentations/session/installationId.js +91 -0
  68. package/dist/cjs/instrumentations/session/installationId.js.map +1 -0
  69. package/dist/cjs/instrumentations/session/sessionAttributes.js +152 -89
  70. package/dist/cjs/instrumentations/session/sessionAttributes.js.map +1 -1
  71. package/dist/cjs/metas/appBuildIdentity.js +95 -0
  72. package/dist/cjs/metas/appBuildIdentity.js.map +1 -0
  73. package/dist/cjs/testUtils/mockDeviceInfo.js +5 -0
  74. package/dist/cjs/testUtils/mockDeviceInfo.js.map +1 -1
  75. package/dist/cjs/testUtils/setupTests.js +7 -0
  76. package/dist/cjs/testUtils/setupTests.js.map +1 -1
  77. package/dist/esm/config/getRNInstrumentations.js +3 -1
  78. package/dist/esm/config/getRNInstrumentations.js.map +1 -1
  79. package/dist/esm/config/makeRNConfig.js +49 -10
  80. package/dist/esm/config/makeRNConfig.js.map +1 -1
  81. package/dist/esm/config/types.js.map +1 -1
  82. package/dist/esm/generated/faroRNPackageMeta.js +1 -1
  83. package/dist/esm/generated/faroRNPackageMeta.js.map +1 -1
  84. package/dist/esm/index.js +1 -1
  85. package/dist/esm/index.js.map +1 -1
  86. package/dist/esm/initialize.js +11 -5
  87. package/dist/esm/initialize.js.map +1 -1
  88. package/dist/esm/instrumentations/anr/anrStack.js +16 -0
  89. package/dist/esm/instrumentations/anr/anrStack.js.map +1 -0
  90. package/dist/esm/instrumentations/anr/instrumentation.js +260 -67
  91. package/dist/esm/instrumentations/anr/instrumentation.js.map +1 -1
  92. package/dist/esm/instrumentations/anr/types.js.map +1 -1
  93. package/dist/esm/instrumentations/console/index.js +5 -1
  94. package/dist/esm/instrumentations/console/index.js.map +1 -1
  95. package/dist/esm/instrumentations/crashReporting/{instrumentation.js → BaseCrashReportingInstrumentation.js} +76 -94
  96. package/dist/esm/instrumentations/crashReporting/BaseCrashReportingInstrumentation.js.map +1 -0
  97. package/dist/esm/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.js +18 -0
  98. package/dist/esm/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.js.map +1 -0
  99. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js +56 -0
  100. package/dist/esm/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.js.map +1 -0
  101. package/dist/esm/instrumentations/crashReporting/android/crashErrorMessage.js +116 -0
  102. package/dist/esm/instrumentations/crashReporting/android/crashErrorMessage.js.map +1 -0
  103. package/dist/esm/instrumentations/crashReporting/android/index.js +4 -0
  104. package/dist/esm/instrumentations/crashReporting/android/index.js.map +1 -0
  105. package/dist/esm/instrumentations/crashReporting/android/nativeTombstoneTrace.js +8 -0
  106. package/dist/esm/instrumentations/crashReporting/android/nativeTombstoneTrace.js.map +1 -0
  107. package/dist/esm/instrumentations/crashReporting/android/parseAndroidCrashTrace.js +174 -0
  108. package/dist/esm/instrumentations/crashReporting/android/parseAndroidCrashTrace.js.map +1 -0
  109. package/dist/esm/instrumentations/crashReporting/index.js +35 -1
  110. package/dist/esm/instrumentations/crashReporting/index.js.map +1 -1
  111. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js +56 -0
  112. package/dist/esm/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.js.map +1 -0
  113. package/dist/esm/instrumentations/crashReporting/ios/crashErrorMessage.js +98 -0
  114. package/dist/esm/instrumentations/crashReporting/ios/crashErrorMessage.js.map +1 -0
  115. package/dist/esm/instrumentations/crashReporting/ios/index.js +4 -0
  116. package/dist/esm/instrumentations/crashReporting/ios/index.js.map +1 -0
  117. package/dist/esm/instrumentations/crashReporting/ios/parseIosCrashTrace.js +44 -0
  118. package/dist/esm/instrumentations/crashReporting/ios/parseIosCrashTrace.js.map +1 -0
  119. package/dist/esm/instrumentations/crashReporting/types.js.map +1 -1
  120. package/dist/esm/instrumentations/errors/index.js +2 -1
  121. package/dist/esm/instrumentations/errors/index.js.map +1 -1
  122. package/dist/esm/instrumentations/session/installationId.js +48 -0
  123. package/dist/esm/instrumentations/session/installationId.js.map +1 -0
  124. package/dist/esm/instrumentations/session/sessionAttributes.js +83 -38
  125. package/dist/esm/instrumentations/session/sessionAttributes.js.map +1 -1
  126. package/dist/esm/metas/appBuildIdentity.js +56 -0
  127. package/dist/esm/metas/appBuildIdentity.js.map +1 -0
  128. package/dist/esm/testUtils/mockDeviceInfo.js +5 -0
  129. package/dist/esm/testUtils/mockDeviceInfo.js.map +1 -1
  130. package/dist/esm/testUtils/setupTests.js +7 -0
  131. package/dist/esm/testUtils/setupTests.js.map +1 -1
  132. package/dist/types/config/makeRNConfig.d.ts +4 -3
  133. package/dist/types/config/types.d.ts +2 -1
  134. package/dist/types/generated/faroRNPackageMeta.d.ts +1 -1
  135. package/dist/types/index.d.ts +2 -2
  136. package/dist/types/initialize.d.ts +4 -2
  137. package/dist/types/instrumentations/anr/anrStack.d.ts +7 -0
  138. package/dist/types/instrumentations/anr/instrumentation.d.ts +23 -28
  139. package/dist/types/instrumentations/anr/types.d.ts +5 -1
  140. package/dist/types/instrumentations/appState/index.d.ts +1 -1
  141. package/dist/types/instrumentations/console/index.d.ts +1 -1
  142. package/dist/types/instrumentations/crashReporting/BaseCrashReportingInstrumentation.d.ts +56 -0
  143. package/dist/types/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.d.ts +12 -0
  144. package/dist/types/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.d.ts +26 -0
  145. package/dist/types/instrumentations/crashReporting/android/crashErrorMessage.d.ts +23 -0
  146. package/dist/types/instrumentations/crashReporting/android/index.d.ts +4 -0
  147. package/dist/types/instrumentations/crashReporting/android/nativeTombstoneTrace.d.ts +2 -0
  148. package/dist/types/instrumentations/crashReporting/android/parseAndroidCrashTrace.d.ts +45 -0
  149. package/dist/types/instrumentations/crashReporting/index.d.ts +25 -2
  150. package/dist/types/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.d.ts +28 -0
  151. package/dist/types/instrumentations/crashReporting/ios/crashErrorMessage.d.ts +14 -0
  152. package/dist/types/instrumentations/crashReporting/ios/index.d.ts +4 -0
  153. package/dist/types/instrumentations/crashReporting/ios/parseIosCrashTrace.d.ts +38 -0
  154. package/dist/types/instrumentations/crashReporting/types.d.ts +18 -51
  155. package/dist/types/instrumentations/frameMonitoring/instrumentation.d.ts +1 -1
  156. package/dist/types/instrumentations/http/index.d.ts +1 -1
  157. package/dist/types/instrumentations/performance/index.d.ts +1 -1
  158. package/dist/types/instrumentations/session/index.d.ts +1 -1
  159. package/dist/types/instrumentations/session/installationId.d.ts +1 -0
  160. package/dist/types/instrumentations/session/sessionAttributes.d.ts +13 -4
  161. package/dist/types/instrumentations/startup/index.d.ts +1 -1
  162. package/dist/types/instrumentations/userActions/index.d.ts +1 -1
  163. package/dist/types/instrumentations/view/index.d.ts +1 -1
  164. package/dist/types/instrumentations/xhr/index.d.ts +1 -1
  165. package/dist/types/metas/appBuildIdentity.d.ts +16 -0
  166. package/dist/types/testUtils/mockFaroReactNativeTracing.d.ts +1 -1
  167. package/dist/types/transports/console/transport.d.ts +1 -1
  168. package/dist/types/transports/fetch/transport.d.ts +1 -1
  169. package/dist/types/transports/offline/transport.d.ts +1 -1
  170. package/package.json +4 -4
  171. package/src/config/getRNInstrumentations.ts +5 -1
  172. package/src/config/makeRNConfig.test.ts +59 -3
  173. package/src/config/makeRNConfig.ts +48 -9
  174. package/src/config/types.ts +2 -1
  175. package/src/generated/faroRNPackageMeta.ts +1 -1
  176. package/src/index.ts +2 -1
  177. package/src/initialize.test.ts +71 -15
  178. package/src/initialize.ts +11 -5
  179. package/src/instrumentations/anr/anrStack.test.ts +27 -0
  180. package/src/instrumentations/anr/anrStack.ts +19 -0
  181. package/src/instrumentations/anr/instrumentation.test.ts +45 -0
  182. package/src/instrumentations/anr/instrumentation.ts +301 -67
  183. package/src/instrumentations/anr/types.ts +7 -1
  184. package/src/instrumentations/console/index.ts +5 -1
  185. package/src/instrumentations/console/instrumentation.test.ts +5 -0
  186. package/src/instrumentations/crashReporting/{instrumentation.ts → BaseCrashReportingInstrumentation.ts} +119 -93
  187. package/src/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.ts +23 -0
  188. package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +62 -0
  189. package/src/instrumentations/crashReporting/android/crashErrorMessage.test.ts +182 -0
  190. package/src/instrumentations/crashReporting/android/crashErrorMessage.ts +135 -0
  191. package/src/instrumentations/crashReporting/android/index.ts +4 -0
  192. package/src/instrumentations/crashReporting/android/nativeTombstoneTrace.test.ts +19 -0
  193. package/src/instrumentations/crashReporting/android/nativeTombstoneTrace.ts +8 -0
  194. package/src/instrumentations/crashReporting/android/parseAndroidCrashTrace.test.ts +232 -0
  195. package/src/instrumentations/crashReporting/android/parseAndroidCrashTrace.ts +235 -0
  196. package/src/instrumentations/crashReporting/index.ts +43 -2
  197. package/src/instrumentations/crashReporting/instrumentation.test.ts +42 -0
  198. package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +62 -0
  199. package/src/instrumentations/crashReporting/ios/crashErrorMessage.test.ts +170 -0
  200. package/src/instrumentations/crashReporting/ios/crashErrorMessage.ts +111 -0
  201. package/src/instrumentations/crashReporting/ios/index.ts +4 -0
  202. package/src/instrumentations/crashReporting/ios/parseIosCrashTrace.test.ts +86 -0
  203. package/src/instrumentations/crashReporting/ios/parseIosCrashTrace.ts +83 -0
  204. package/src/instrumentations/crashReporting/types.ts +19 -58
  205. package/src/instrumentations/errors/index.ts +2 -1
  206. package/src/instrumentations/errors/instrumentation.test.ts +8 -2
  207. package/src/instrumentations/errors/stackTraceParser.test.ts +1 -1
  208. package/src/instrumentations/session/installationId.ts +43 -0
  209. package/src/instrumentations/session/sessionAttributes.test.ts +260 -30
  210. package/src/instrumentations/session/sessionAttributes.ts +112 -22
  211. package/src/metas/appBuildIdentity.test.ts +48 -0
  212. package/src/metas/appBuildIdentity.ts +50 -0
  213. package/src/testUtils/mockDeviceInfo.ts +5 -0
  214. package/src/testUtils/setupTests.ts +7 -0
  215. package/dist/cjs/instrumentations/crashReporting/instrumentation.js.map +0 -1
  216. package/dist/esm/instrumentations/crashReporting/instrumentation.js.map +0 -1
  217. package/dist/types/instrumentations/crashReporting/instrumentation.d.ts +0 -58
@@ -1,9 +1,11 @@
1
- import { NativeModules, Platform } from 'react-native';
1
+ import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
2
2
 
3
3
  import { BaseInstrumentation, VERSION } from '@grafana/faro-core';
4
4
 
5
+ import { normalizeJavaStackTraceForRetrace, parseAndroidCrashTrace } from '../crashReporting/android';
5
6
  import { ErrorMechanism } from '../errors/const';
6
7
 
8
+ import { isInvalidAnrCaptureStack } from './anrStack';
7
9
  import type { ANREvent, ANRInstrumentationOptions } from './types';
8
10
 
9
11
  /**
@@ -12,38 +14,26 @@ import type { ANREvent, ANRInstrumentationOptions } from './types';
12
14
  const DEFAULT_TIMEOUT = 5000;
13
15
 
14
16
  /**
15
- * Default polling interval for ANR status (60 seconds, matching Flutter SDK)
17
+ * Default polling interval for ANR status. Kept short so debug ANRs surface without
18
+ * waiting a full minute; native also emits onANRDetected for immediate delivery.
16
19
  */
17
- const DEFAULT_POLLING_INTERVAL = 60000;
20
+ const DEFAULT_POLLING_INTERVAL = 10000;
21
+
22
+ /** Aligns with native FaroAnrCache.MAX_TIMESTAMP_DELTA_MS */
23
+ const TIMESTAMP_DEDUP_WINDOW_MS = 10_000;
24
+
25
+ /** ApplicationExitInfo ANR traces (Android 11 / API 30+). */
26
+ const APP_EXIT_INFO_MIN_SDK = 30;
18
27
 
19
28
  /**
20
29
  * ANR (Application Not Responding) Detection Instrumentation.
21
30
  *
22
- * Detects when the main/UI thread is blocked for extended periods on Android.
23
- * Uses a background thread that posts tasks to the main thread and monitors
24
- * if they complete within the timeout.
25
- *
26
- * **Note**: ANR detection is only available on Android. iOS does not have
27
- * the same ANR concept as Android's system watchdog.
28
- *
29
- * Sends telemetry via Faro API:
30
- * - Measurement: `anr` with `anr_count` value (for dashboards)
31
- * - Error: Each ANR with `type: 'ANR'`, stack trace, duration, timestamp (Sentry-aligned)
31
+ * On Android 11+, historical ANRs are read from ApplicationExitInfo (Sentry AnrV2
32
+ * style) on the next launch. ANRTracker remains a pre-API-30 fallback and a
33
+ * persistence backup when the OS trace stream is empty.
32
34
  *
33
- * @example
34
- * ```typescript
35
- * import { initializeFaro, ANRInstrumentation } from '@grafana/faro-react-native';
36
- *
37
- * initializeFaro({
38
- * url: 'https://collector.example.com',
39
- * instrumentations: [
40
- * new ANRInstrumentation({
41
- * timeout: 5000, // 5 second threshold
42
- * pollingInterval: 60000, // Poll every 60 seconds
43
- * }),
44
- * ],
45
- * });
46
- * ```
35
+ * ANRs stay out of CrashReportingInstrumentation so they are not replayed as
36
+ * generic previous-session `crash` rows with empty messages.
47
37
  */
48
38
  export class ANRInstrumentation extends BaseInstrumentation {
49
39
  readonly name = '@grafana/faro-react-native:instrumentation-anr';
@@ -51,6 +41,9 @@ export class ANRInstrumentation extends BaseInstrumentation {
51
41
 
52
42
  private readonly options: Required<ANRInstrumentationOptions>;
53
43
  private pollingIntervalId: ReturnType<typeof setInterval> | null = null;
44
+ private anrEventSubscription: ReturnType<NativeEventEmitter['addListener']> | null = null;
45
+ private readonly reportedAnrTimestamps = new Set<number>();
46
+ private readonly preferAppExitInfoAnr: boolean;
54
47
 
55
48
  constructor(options: ANRInstrumentationOptions = {}) {
56
49
  super();
@@ -58,10 +51,10 @@ export class ANRInstrumentation extends BaseInstrumentation {
58
51
  timeout: options.timeout ?? DEFAULT_TIMEOUT,
59
52
  pollingInterval: options.pollingInterval ?? DEFAULT_POLLING_INTERVAL,
60
53
  };
54
+ this.preferAppExitInfoAnr = Platform.OS === 'android' && Number(Platform.Version) >= APP_EXIT_INFO_MIN_SDK;
61
55
  }
62
56
 
63
57
  initialize(): void {
64
- // ANR detection is only available on Android
65
58
  if (Platform.OS !== 'android') {
66
59
  this.logDebug('ANR detection is only available on Android');
67
60
  return;
@@ -75,12 +68,18 @@ export class ANRInstrumentation extends BaseInstrumentation {
75
68
 
76
69
  this.logDebug('Initializing ANR detection instrumentation');
77
70
 
78
- // Start native ANR tracking
71
+ void this.drainAllAnrSources(nativeModule);
72
+
79
73
  this.startNativeTracking(nativeModule);
80
74
 
81
- // Set up polling for ANR status
75
+ const emitter = new NativeEventEmitter(nativeModule);
76
+ this.anrEventSubscription = emitter.addListener('onANRDetected', (anrJson: string) => {
77
+ void this.handleLiveAnrEvent(nativeModule, anrJson);
78
+ });
79
+
80
+ void this.checkANRStatus(nativeModule);
82
81
  this.pollingIntervalId = setInterval(() => {
83
- this.checkANRStatus(nativeModule);
82
+ void this.checkANRStatus(nativeModule);
84
83
  }, this.options.pollingInterval);
85
84
  }
86
85
 
@@ -107,68 +106,303 @@ export class ANRInstrumentation extends BaseInstrumentation {
107
106
  }
108
107
  }
109
108
 
110
- private async checkANRStatus(nativeModule: typeof NativeModules.FaroReactNativeModule): Promise<void> {
109
+ private async drainAllAnrSources(nativeModule: typeof NativeModules.FaroReactNativeModule): Promise<void> {
110
+ await this.drainHistoricalAnrs(nativeModule);
111
+ await this.drainPendingANRs(nativeModule);
112
+ }
113
+
114
+ private async drainHistoricalAnrs(nativeModule: typeof NativeModules.FaroReactNativeModule): Promise<void> {
115
+ if (typeof nativeModule.getHistoricalAnrReports !== 'function') {
116
+ return;
117
+ }
118
+
111
119
  try {
112
- if (typeof nativeModule.getANRStatus !== 'function') {
120
+ const anrList = (await nativeModule.getHistoricalAnrReports()) as string[] | null;
121
+ if (!anrList?.length) {
113
122
  return;
114
123
  }
115
124
 
116
- const anrList = (await nativeModule.getANRStatus()) as string[] | null;
125
+ const ackTimestamps: number[] = [];
126
+ let reportedCount = 0;
117
127
 
118
- if (anrList && anrList.length > 0) {
119
- // Push measurement for ANR count (matching Flutter pattern)
128
+ for (const anrJson of anrList) {
129
+ try {
130
+ const anr = JSON.parse(anrJson) as ANREvent;
131
+ if (this.reportANRIfNew(anr)) {
132
+ reportedCount += 1;
133
+ }
134
+ ackTimestamps.push(anr.timestamp);
135
+ await this.acknowledgeNearbyPendingAnrs(nativeModule, anr.timestamp);
136
+ } catch (error) {
137
+ this.logError('Failed to parse historical ANR report', error);
138
+ }
139
+ }
140
+
141
+ if (reportedCount > 0) {
120
142
  this.api.pushMeasurement(
121
143
  {
122
144
  type: 'anr',
123
- values: { anr_count: anrList.length },
145
+ values: { anr_count: reportedCount },
124
146
  },
125
147
  { skipDedupe: true }
126
148
  );
149
+ this.logDebug(`Reported ${reportedCount} historical ANR event(s) from ApplicationExitInfo`);
150
+ }
151
+ } catch (error) {
152
+ this.logError('Failed to drain historical ANRs', error);
153
+ }
154
+ }
127
155
 
128
- // Push each ANR as an error with type='ANR' for filtering (Sentry-aligned)
129
- for (const anrJson of anrList) {
130
- try {
131
- const anr = JSON.parse(anrJson) as ANREvent;
132
-
133
- this.api.pushError(new Error('ANR (Application Not Responding)'), {
134
- context: {
135
- duration: String(anr.duration),
136
- mechanism: ErrorMechanism.ANR,
137
- stacktrace: anr.stacktrace,
138
- timestamp: String(anr.timestamp),
139
- },
140
- type: 'ANR',
141
- });
142
- } catch {
143
- // If parsing fails, still log the raw ANR
144
- this.api.pushError(new Error('ANR (Application Not Responding)'), {
145
- context: {
146
- mechanism: ErrorMechanism.ANR,
147
- raw: anrJson,
148
- },
149
- type: 'ANR',
150
- });
151
- }
156
+ private async drainPendingANRs(nativeModule: typeof NativeModules.FaroReactNativeModule): Promise<void> {
157
+ const anrList = await this.fetchPendingANRs(nativeModule);
158
+ if (!anrList?.length) {
159
+ return;
160
+ }
161
+
162
+ await this.reportAndAcknowledgePendingANRs(nativeModule, anrList);
163
+ this.logDebug(`Processed ${anrList.length} persisted ANR event(s) from tracker cache`);
164
+ }
165
+
166
+ private async handleLiveAnrEvent(
167
+ nativeModule: typeof NativeModules.FaroReactNativeModule,
168
+ anrJson: string
169
+ ): Promise<void> {
170
+ try {
171
+ const anr = JSON.parse(anrJson) as ANREvent;
172
+
173
+ if (this.preferAppExitInfoAnr) {
174
+ // Process death + ApplicationExitInfo on next launch delivers the main-thread dump.
175
+ this.logDebug('Deferring live ANR to ApplicationExitInfo on next launch');
176
+ return;
177
+ }
178
+
179
+ if (!this.shouldReportTrackerAnr(anr)) {
180
+ await this.acknowledgeANRs(nativeModule, [anr.timestamp]);
181
+ return;
182
+ }
183
+
184
+ if (this.reportANRIfNew(anr)) {
185
+ this.api.pushMeasurement(
186
+ {
187
+ type: 'anr',
188
+ values: { anr_count: 1 },
189
+ },
190
+ { skipDedupe: true }
191
+ );
192
+ }
193
+ await this.acknowledgeANRs(nativeModule, [anr.timestamp]);
194
+ } catch (error) {
195
+ this.logError('Failed to parse native ANR event', error);
196
+ }
197
+ }
198
+
199
+ private async fetchPendingANRs(nativeModule: typeof NativeModules.FaroReactNativeModule): Promise<string[] | null> {
200
+ try {
201
+ if (typeof nativeModule.getPendingANRs === 'function') {
202
+ return (await nativeModule.getPendingANRs()) as string[] | null;
203
+ }
204
+
205
+ if (typeof nativeModule.getANRStatus === 'function') {
206
+ return (await nativeModule.getANRStatus()) as string[] | null;
207
+ }
208
+ } catch (error) {
209
+ this.logError('Failed to fetch pending ANRs', error);
210
+ }
211
+
212
+ return null;
213
+ }
214
+
215
+ private async acknowledgeANRs(
216
+ nativeModule: typeof NativeModules.FaroReactNativeModule,
217
+ timestamps: number[]
218
+ ): Promise<void> {
219
+ if (!timestamps.length || typeof nativeModule.acknowledgeANRs !== 'function') {
220
+ return;
221
+ }
222
+
223
+ try {
224
+ await nativeModule.acknowledgeANRs(timestamps);
225
+ } catch (error) {
226
+ this.logError('Failed to acknowledge ANRs', error);
227
+ }
228
+ }
229
+
230
+ private async acknowledgeNearbyPendingAnrs(
231
+ nativeModule: typeof NativeModules.FaroReactNativeModule,
232
+ exitTimestamp: number
233
+ ): Promise<void> {
234
+ const pending = await this.fetchPendingANRs(nativeModule);
235
+ if (!pending?.length) {
236
+ return;
237
+ }
238
+
239
+ const ackTimestamps: number[] = [];
240
+ for (const anrJson of pending) {
241
+ try {
242
+ const anr = JSON.parse(anrJson) as ANREvent;
243
+ if (this.isNearbyTimestamp(anr.timestamp, exitTimestamp)) {
244
+ ackTimestamps.push(anr.timestamp);
245
+ }
246
+ } catch {
247
+ // ignore malformed cache entries
248
+ }
249
+ }
250
+
251
+ if (ackTimestamps.length > 0) {
252
+ await this.acknowledgeANRs(nativeModule, ackTimestamps);
253
+ }
254
+ }
255
+
256
+ private async reportAndAcknowledgePendingANRs(
257
+ nativeModule: typeof NativeModules.FaroReactNativeModule,
258
+ anrList: string[]
259
+ ): Promise<void> {
260
+ const ackTimestamps: number[] = [];
261
+ let reportedCount = 0;
262
+
263
+ for (const anrJson of anrList) {
264
+ try {
265
+ const anr = JSON.parse(anrJson) as ANREvent;
266
+
267
+ if (this.shouldReportTrackerAnr(anr) && this.reportANRIfNew(anr)) {
268
+ reportedCount += 1;
152
269
  }
153
270
 
154
- this.logDebug(`Recorded ${anrList.length} ANR event(s)`);
271
+ ackTimestamps.push(anr.timestamp);
272
+ } catch {
273
+ const error = new Error('ANR (Application Not Responding)');
274
+ error.stack = undefined;
275
+ this.api.pushError(error, {
276
+ context: {
277
+ mechanism: ErrorMechanism.ANR,
278
+ raw: anrJson,
279
+ },
280
+ fatal: true,
281
+ type: 'ANR',
282
+ });
283
+ }
284
+ }
285
+
286
+ if (reportedCount > 0) {
287
+ this.api.pushMeasurement(
288
+ {
289
+ type: 'anr',
290
+ values: { anr_count: reportedCount },
291
+ },
292
+ { skipDedupe: true }
293
+ );
294
+ }
295
+
296
+ if (ackTimestamps.length > 0) {
297
+ await this.acknowledgeANRs(nativeModule, ackTimestamps);
298
+ }
299
+ }
300
+
301
+ private shouldReportTrackerAnr(anr: ANREvent): boolean {
302
+ if (this.isNearbyReportedTimestamp(anr.timestamp)) {
303
+ return false;
304
+ }
305
+
306
+ if (isInvalidAnrCaptureStack(anr.stacktrace)) {
307
+ return false;
308
+ }
309
+
310
+ // On API 30+, ApplicationExitInfo is preferred on launch; tracker pending entries
311
+ // are reported only when historical did not already cover the same incident.
312
+ return true;
313
+ }
314
+
315
+ private reportANRIfNew(anr: ANREvent): boolean {
316
+ if (this.isNearbyReportedTimestamp(anr.timestamp)) {
317
+ return false;
318
+ }
319
+
320
+ if (isInvalidAnrCaptureStack(anr.stacktrace)) {
321
+ return false;
322
+ }
323
+
324
+ this.rememberReportedTimestamp(anr.timestamp);
325
+ this.reportANR(anr);
326
+ return true;
327
+ }
328
+
329
+ private rememberReportedTimestamp(timestamp: number): void {
330
+ this.reportedAnrTimestamps.add(timestamp);
331
+ }
332
+
333
+ private isNearbyReportedTimestamp(timestamp: number): boolean {
334
+ for (const reported of Array.from(this.reportedAnrTimestamps)) {
335
+ if (this.isNearbyTimestamp(timestamp, reported)) {
336
+ return true;
337
+ }
338
+ }
339
+ return false;
340
+ }
341
+
342
+ private isNearbyTimestamp(a: number, b: number): boolean {
343
+ if (a <= 0 || b <= 0) {
344
+ return false;
345
+ }
346
+ return Math.abs(a - b) <= TIMESTAMP_DEDUP_WINDOW_MS;
347
+ }
348
+
349
+ private reportANR(anr: ANREvent): void {
350
+ const rawStacktrace = anr.stacktrace?.trim() ?? '';
351
+ const traceForRetrace = rawStacktrace ? normalizeJavaStackTraceForRetrace(rawStacktrace) : '';
352
+ const parsed = traceForRetrace ? parseAndroidCrashTrace(traceForRetrace) : null;
353
+
354
+ const message = anr.description?.trim() || 'ANR (Application Not Responding)';
355
+ const error = new Error(message);
356
+ error.stack = undefined;
357
+
358
+ const context: Record<string, string> = {
359
+ duration: String(anr.duration),
360
+ mechanism: ErrorMechanism.ANR,
361
+ timestamp: String(anr.timestamp),
362
+ };
363
+ if (anr.source) {
364
+ context['source'] = anr.source;
365
+ }
366
+ if (traceForRetrace) {
367
+ context['stacktrace'] = traceForRetrace;
368
+ }
369
+
370
+ const stackFrames = parsed?.frames ?? [];
371
+ if (!stackFrames.length && rawStacktrace) {
372
+ this.logWarn('ANR main-thread stack present but no Java/Kotlin frames parsed');
373
+ }
374
+
375
+ this.api.pushError(error, {
376
+ type: 'ANR',
377
+ context,
378
+ fatal: true,
379
+ ...(stackFrames.length ? { stackFrames } : {}),
380
+ });
381
+ }
382
+
383
+ private async checkANRStatus(nativeModule: typeof NativeModules.FaroReactNativeModule): Promise<void> {
384
+ try {
385
+ const anrList = await this.fetchPendingANRs(nativeModule);
386
+
387
+ if (anrList && anrList.length > 0) {
388
+ await this.reportAndAcknowledgePendingANRs(nativeModule, anrList);
389
+ this.logDebug(`Polled ${anrList.length} ANR event(s) from pending cache`);
155
390
  }
156
391
  } catch (error) {
157
392
  this.logError('Failed to check ANR status', error);
158
393
  }
159
394
  }
160
395
 
161
- /**
162
- * Clean up resources when instrumentation is disabled.
163
- */
164
396
  unpatch(): void {
165
- // Stop polling
166
397
  if (this.pollingIntervalId !== null) {
167
398
  clearInterval(this.pollingIntervalId);
168
399
  this.pollingIntervalId = null;
169
400
  }
170
401
 
171
- // Stop native tracking
402
+ this.anrEventSubscription?.remove();
403
+ this.anrEventSubscription = null;
404
+ this.reportedAnrTimestamps.clear();
405
+
172
406
  if (Platform.OS === 'android') {
173
407
  const nativeModule = this.getNativeModule();
174
408
  if (nativeModule && typeof nativeModule.stopANRTracking === 'function') {
@@ -11,7 +11,7 @@ export interface ANRInstrumentationOptions {
11
11
 
12
12
  /**
13
13
  * Interval in milliseconds for polling ANR status from native.
14
- * Default: 60000 (60 seconds, matching Flutter SDK's default)
14
+ * Default: 10000 (10 seconds). Native also emits onANRDetected immediately.
15
15
  */
16
16
  pollingInterval?: number;
17
17
  }
@@ -39,4 +39,10 @@ export interface ANREvent {
39
39
  * Duration of the ANR in milliseconds
40
40
  */
41
41
  duration: number;
42
+
43
+ /** Human-readable ANR description from ApplicationExitInfo when available. */
44
+ description?: string;
45
+
46
+ /** `AppExitInfo` (preferred) or `ANRTracker` (fallback). */
47
+ source?: string;
42
48
  }
@@ -68,11 +68,15 @@ export class ConsoleInstrumentation extends BaseInstrumentation {
68
68
 
69
69
  if (value && !type && !stackFrames) {
70
70
  // Simple error without stack frames
71
- this.api.pushError(new Error(ConsoleInstrumentation.consoleErrorPrefix + value), { context });
71
+ this.api.pushError(new Error(ConsoleInstrumentation.consoleErrorPrefix + value), {
72
+ context,
73
+ fatal: false,
74
+ });
72
75
  } else {
73
76
  // Error with type and/or stack frames (type from error.name, matches Web SDK)
74
77
  this.api.pushError(new Error(ConsoleInstrumentation.consoleErrorPrefix + (value ?? '')), {
75
78
  context,
79
+ fatal: false,
76
80
  type,
77
81
  stackFrames,
78
82
  });
@@ -46,11 +46,13 @@ describe('ConsoleInstrumentation', () => {
46
46
  expect((mockTransport.items[0] as TransportItem<ExceptionEvent>)?.payload.value).toBe(
47
47
  'console.error: console.error no 1'
48
48
  );
49
+ expect((mockTransport.items[0] as TransportItem<ExceptionEvent>)?.payload.fatal).toBe(false);
49
50
  expect((mockTransport.items[0] as TransportItem<ExceptionEvent>)?.payload.context?.mechanism).toBe('console');
50
51
  expect((mockTransport.items[1] as TransportItem<ExceptionEvent>)?.payload.type).toBe('Error');
51
52
  expect((mockTransport.items[1] as TransportItem<ExceptionEvent>)?.payload.value).toBe(
52
53
  'console.error: with object {"foo":"bar","baz":"bam"}'
53
54
  );
55
+ expect((mockTransport.items[1] as TransportItem<ExceptionEvent>)?.payload.fatal).toBe(false);
54
56
  });
55
57
 
56
58
  it('sends a faro error serialized with error serializer when console.error is called', () => {
@@ -80,10 +82,12 @@ describe('ConsoleInstrumentation', () => {
80
82
  expect((mockTransport.items[0] as TransportItem<ExceptionEvent>)?.payload.value).toBe(
81
83
  'console.error: console.error no 1'
82
84
  );
85
+ expect((mockTransport.items[0] as TransportItem<ExceptionEvent>)?.payload.fatal).toBe(false);
83
86
  expect((mockTransport.items[1] as TransportItem<ExceptionEvent>)?.payload.type).toBe('Error');
84
87
  expect((mockTransport.items[1] as TransportItem<ExceptionEvent>)?.payload.value).toBe(
85
88
  'console.error: with object ' + stringifyExternalJson(context)
86
89
  );
90
+ expect((mockTransport.items[1] as TransportItem<ExceptionEvent>)?.payload.fatal).toBe(false);
87
91
  });
88
92
 
89
93
  it('Handles objects with circular references', () => {
@@ -244,6 +248,7 @@ describe('ConsoleInstrumentation', () => {
244
248
  expect((mockTransport.items[2] as TransportItem<ExceptionEvent>)?.payload.value).toBe(
245
249
  'console.error: ' + errorLogMessage
246
250
  );
251
+ expect((mockTransport.items[2] as TransportItem<ExceptionEvent>)?.payload.fatal).toBe(false);
247
252
  });
248
253
 
249
254
  it('unpatch restores original console methods', () => {