@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,47 +1,28 @@
1
1
  import { NativeModules } from 'react-native';
2
2
 
3
- import { BaseInstrumentation, VERSION } from '@grafana/faro-core';
4
-
5
- import { ErrorMechanism } from '../errors/const';
3
+ import { BaseInstrumentation } from '@grafana/faro-core';
6
4
 
5
+ import { looksLikeNativeTombstoneTrace } from './android/nativeTombstoneTrace';
7
6
  import type { CrashReport, CrashReportingOptions } from './types';
8
7
 
9
8
  /**
10
- * Crash Reporting Instrumentation (Experimental).
11
- *
12
- * Retrieves crash reports from previous app sessions and sends them via Faro API.
13
- *
14
- * **Platform Support**:
15
- * - **Android**: Uses ApplicationExitInfo API (Android 11+ / API 30+)
16
- * Captures: CRASH, CRASH_NATIVE, ANR, LOW_MEMORY, EXCESSIVE_RESOURCE_USAGE
17
- * - **iOS**: Uses PLCrashReporter (requires adding dependency to podspec)
18
- * Captures: Signal crashes (SIGSEGV, SIGABRT, etc.) and Mach exceptions
9
+ * Abstract base class for platform-specific crash reporting implementations.
19
10
  *
20
- * **Note**: This instrumentation only retrieves crash data from previous sessions.
21
- * The crash itself terminates the app, so the report is sent on next app launch.
22
- *
23
- * @example
24
- * ```typescript
25
- * import { initializeFaro, CrashReportingInstrumentation } from '@grafana/faro-react-native';
26
- *
27
- * initializeFaro({
28
- * url: 'https://collector.example.com',
29
- * instrumentations: [
30
- * new CrashReportingInstrumentation(),
31
- * ],
32
- * });
33
- * ```
11
+ * Defines the common contract that both Android and iOS implementations must follow.
12
+ * Handles the shared logic for retrieving crash reports from native modules and
13
+ * processing them, while delegating platform-specific parsing and validation to subclasses.
34
14
  */
35
- export class CrashReportingInstrumentation extends BaseInstrumentation {
36
- readonly name = '@grafana/faro-react-native:instrumentation-crash';
37
- readonly version = VERSION;
15
+ export abstract class BaseCrashReportingInstrumentation extends BaseInstrumentation {
16
+ abstract override readonly name: string;
17
+ abstract override readonly version: string;
38
18
 
39
- private readonly options: Required<CrashReportingOptions>;
19
+ protected readonly options: { enabled: boolean; releaseBundleFilename?: string };
40
20
 
41
21
  constructor(options: CrashReportingOptions = {}) {
42
22
  super();
43
23
  this.options = {
44
24
  enabled: options.enabled ?? true,
25
+ releaseBundleFilename: options.releaseBundleFilename,
45
26
  };
46
27
  }
47
28
 
@@ -59,37 +40,56 @@ export class CrashReportingInstrumentation extends BaseInstrumentation {
59
40
 
60
41
  this.logDebug('Initializing crash reporting instrumentation');
61
42
 
62
- // Wait for session attributes to be populated before sending crash reports
63
- // This ensures crash reports include full session metadata
43
+ // Wait for session attributes before sending crash reports
64
44
  await this.waitForSessionAttributes();
65
45
 
66
- // Get crash reports from previous session
46
+ // Get and process crash reports from previous session
67
47
  await this.processCrashReports(nativeModule);
68
48
  }
69
49
 
70
50
  /**
71
- * Wait for session attributes to be populated.
72
- *
73
- * Crash reports are sent in a new session, so we need to wait for
74
- * SessionInstrumentation to finish collecting device info, OS version, etc.
75
- * before sending the crash report. Otherwise, the crash report will be missing
76
- * session metadata.
77
- *
78
- * This checks for multiple device-specific attributes that are collected
79
- * asynchronously (device_id, device_os_detail, device_model_name) to ensure
80
- * the async getSessionAttributes() call has completed.
81
- *
82
- * @param maxWaitMs Maximum time to wait in milliseconds (default 10000ms)
51
+ * Platform-specific: Parse a crash trace string into structured stack frames.
52
+ */
53
+ protected abstract parseCrashTrace(trace: string): any | null;
54
+
55
+ /**
56
+ * Platform-specific: Resolve human-readable error message from crash data.
57
+ */
58
+ protected abstract resolveCrashErrorMessage(crash: CrashReport, parsedTrace: any | null): string;
59
+
60
+ /**
61
+ * Platform-specific: Determine if a crash report should be skipped.
62
+ */
63
+ protected abstract shouldSkipCrashReport(crash: CrashReport): boolean;
64
+
65
+ /**
66
+ * Platform-specific: Extract stack frames from parsed trace.
67
+ */
68
+ protected abstract getStackFrames(parsedTrace: any | null): any[];
69
+
70
+ /**
71
+ * Platform-specific: Build platform-specific context from parsed trace.
72
+ */
73
+ protected abstract buildPlatformContext(parsedTrace: any | null, context: Record<string, string>): void;
74
+
75
+ /**
76
+ * Platform-specific: Get warning message when trace parsing fails.
77
+ */
78
+ protected abstract getParseFailureWarning(): string;
79
+
80
+ /**
81
+ * Platform-specific: Get warning message when trace is unavailable.
83
82
  */
83
+ protected abstract getTraceUnavailableWarning(): string;
84
+
84
85
  private async waitForSessionAttributes(maxWaitMs = 10000): Promise<void> {
85
86
  const startTime = Date.now();
86
- const pollInterval = 200; // Check every 200ms
87
+ const pollInterval = 200;
87
88
  let checkCount = 0;
88
89
 
89
90
  while (Date.now() - startTime < maxWaitMs) {
90
91
  checkCount++;
91
92
  try {
92
- // Check if session attributes are populated
93
93
  const sessionAttrs = this.metas?.value?.session?.attributes;
94
94
 
95
95
  if (sessionAttrs) {
@@ -98,7 +98,7 @@ export class CrashReportingInstrumentation extends BaseInstrumentation {
98
98
 
99
99
  // Look for multiple device-specific attributes that indicate full async collection is done.
100
100
  // getSessionAttributes() collects these asynchronously:
101
- // - device_id (async getDeviceId)
101
+ // - device_id (SDK installation id, kept as a flat attr during migration)
102
102
  // - device_os_detail (async getDeviceOsDetail)
103
103
  // - device_model_name (DeviceInfo.getDeviceNameSync)
104
104
  // All three should be present when collection is complete.
@@ -120,15 +120,12 @@ export class CrashReportingInstrumentation extends BaseInstrumentation {
120
120
  this.logDebug(`Check #${checkCount}: No session attributes available yet`);
121
121
  }
122
122
  } catch (error) {
123
- // Continue waiting if we can't access metas yet
124
123
  this.logDebug(`Check #${checkCount}: Error accessing metas: ${error}`);
125
124
  }
126
125
 
127
- // Wait before next check
128
126
  await new Promise((resolve) => setTimeout(resolve, pollInterval));
129
127
  }
130
128
 
131
- // Timeout reached - log warning but continue
132
129
  const finalAttrs = this.metas?.value?.session?.attributes;
133
130
  const finalAttrCount = finalAttrs ? Object.keys(finalAttrs).length : 0;
134
131
  const finalAttrKeys = finalAttrs ? JSON.stringify(Object.keys(finalAttrs)) : 'none';
@@ -164,20 +161,37 @@ export class CrashReportingInstrumentation extends BaseInstrumentation {
164
161
 
165
162
  this.logDebug(`Processing ${crashReports.length} crash report(s) from previous session`);
166
163
 
164
+ const reportedTimestamps = new Set<number>();
165
+
167
166
  for (const crashJson of crashReports) {
168
167
  try {
169
168
  this.logDebug(`Parsing crash report JSON: ${crashJson.substring(0, 200)}...`);
170
169
  const crash = JSON.parse(crashJson) as CrashReport;
170
+
171
+ if (crash.timestamp && reportedTimestamps.has(crash.timestamp)) {
172
+ this.logDebug(`Skipping duplicate crash report at ${crash.timestamp}`);
173
+ continue;
174
+ }
175
+
176
+ if (this.shouldSkipCrashReport(crash)) {
177
+ this.logDebug(`Skipping crash report at ${crash.timestamp} (platform-specific skip logic)`);
178
+ continue;
179
+ }
180
+
181
+ if (crash.timestamp) {
182
+ reportedTimestamps.add(crash.timestamp);
183
+ }
184
+
171
185
  this.sendCrashReport(crash);
172
186
  } catch (parseError) {
173
- // If parsing fails, still try to report something
174
187
  this.logError('Failed to parse crash report JSON', parseError);
175
188
  this.api.pushError(new Error('Application crash (parse error)'), {
176
189
  context: {
177
- mechanism: ErrorMechanism.CRASH,
190
+ mechanism: 'crash',
178
191
  parseError: String(parseError),
179
- raw: crashJson.substring(0, 500), // Limit size
192
+ raw: crashJson.substring(0, 500),
180
193
  },
194
+ fatal: true,
181
195
  type: 'crash',
182
196
  });
183
197
  }
@@ -188,12 +202,17 @@ export class CrashReportingInstrumentation extends BaseInstrumentation {
188
202
  }
189
203
 
190
204
  private sendCrashReport(crash: CrashReport): void {
191
- const errorMessage = this.getErrorMessage(crash);
205
+ // Platform-specific parsing
206
+ const parsedTrace = crash.trace ? this.parseCrashTrace(crash.trace) : null;
207
+ const errorMessage = this.resolveCrashErrorMessage(crash, parsedTrace).trim() || 'Application crash';
208
+
209
+ // Use a message-only Error so pushError does not capture the JS reporter stack
192
210
  const error = new Error(errorMessage);
211
+ error.stack = undefined;
193
212
 
194
- // Build context from crash data (matching Flutter pattern)
213
+ // Build context from crash data
195
214
  const context: Record<string, string> = {
196
- mechanism: ErrorMechanism.CRASH,
215
+ mechanism: 'crash',
197
216
  };
198
217
 
199
218
  if (crash.trace) {
@@ -218,48 +237,55 @@ export class CrashReportingInstrumentation extends BaseInstrumentation {
218
237
  context['importance'] = String(crash.importance);
219
238
  }
220
239
 
221
- // Push as error via Faro API (matching Flutter pattern)
240
+ // Platform-specific context
241
+ this.buildPlatformContext(parsedTrace, context);
242
+
243
+ // Platform-specific stack frames
244
+ const stackFrames = this.getStackFrames(parsedTrace);
245
+
246
+ // Warnings for missing/unparseable traces
247
+ if (!stackFrames.length && crash.trace) {
248
+ this.logWarn(this.getParseFailureWarning());
249
+ } else if (!crash.trace) {
250
+ this.logWarn(this.getTraceUnavailableWarning());
251
+ } else if (looksLikeNativeTombstoneTrace(crash.trace)) {
252
+ const framePreview = crash.trace
253
+ .split('\n')
254
+ .map((line) => line.trim())
255
+ .find((line) => /#\d+\s+pc\s+/i.test(line));
256
+ this.logInfo(
257
+ `[Faro crash native] Sending tombstone trace (${crash.trace.split('\n').length} lines) preview=${framePreview ?? 'n/a'}`
258
+ );
259
+ }
260
+
222
261
  this.api.pushError(error, {
223
- type: 'crash',
262
+ stackFrames,
224
263
  context,
264
+ type: 'crash',
265
+ fatal: true,
225
266
  });
226
267
 
227
- this.logDebug(`Reported crash: ${crash.reason} at ${crash.timestamp}`);
268
+ this.logDebug(`Sent crash report: ${errorMessage}`);
228
269
  }
229
270
 
230
- /**
231
- * Build error message matching Flutter SDK format:
232
- * "{reason}: {description}, status: {status}"
233
- */
234
- private getErrorMessage(crash: CrashReport): string {
235
- const reason = crash.reason || 'UNKNOWN';
236
- const status = crash.status ?? 0;
237
-
238
- let description: string;
239
- switch (crash.reason) {
240
- case 'ANR':
241
- description = 'Application Not Responding';
242
- break;
243
- case 'CRASH':
244
- description = 'Application crash (Java/Kotlin)';
245
- break;
246
- case 'CRASH_NATIVE':
247
- description = 'Application crash (Native)';
248
- break;
249
- case 'LOW_MEMORY':
250
- description = 'Application terminated due to low memory';
251
- break;
252
- case 'EXCESSIVE_RESOURCE_USAGE':
253
- description = 'Application terminated due to excessive resource usage';
254
- break;
255
- case 'INITIALIZATION_FAILURE':
256
- description = 'Application failed to initialize';
257
- break;
258
- default:
259
- description = crash.description || 'Application crash';
260
- break;
261
- }
271
+ // Logging helpers
272
+ override logDebug(message: string, ...args: any[]): void {
273
+ this.internalLogger?.debug?.(message, ...args);
274
+ }
275
+
276
+ override logInfo(message: string, ...args: any[]): void {
277
+ this.internalLogger?.info?.(message, ...args);
278
+ }
279
+
280
+ override logWarn(message: string, ...args: any[]): void {
281
+ this.internalLogger?.warn?.(message, ...args);
282
+ }
283
+
284
+ override logError(message: string, ...args: any[]): void {
285
+ this.internalLogger?.error?.(message, ...args);
286
+ }
262
287
 
263
- return `${reason}: ${description}, status: ${status}`;
288
+ unpatch(): void {
289
+ // No patching to undo
264
290
  }
265
291
  }
@@ -0,0 +1,23 @@
1
+ import { BaseInstrumentation, VERSION } from '@grafana/faro-core';
2
+
3
+ import type { CrashReportingOptions } from './types';
4
+
5
+ /**
6
+ * Crash reporting stub for platforms other than Android and iOS (e.g. web, macOS, Windows).
7
+ */
8
+ export class NoOpCrashReportingInstrumentation extends BaseInstrumentation {
9
+ readonly name = '@grafana/faro-react-native:instrumentation-crash-noop';
10
+ readonly version = VERSION;
11
+
12
+ constructor(_options: CrashReportingOptions = {}) {
13
+ super();
14
+ }
15
+
16
+ initialize(): void {
17
+ this.logDebug('Crash reporting is not supported on this platform');
18
+ }
19
+
20
+ unpatch(): void {
21
+ // No patching to undo
22
+ }
23
+ }
@@ -0,0 +1,62 @@
1
+ import { VERSION } from '@grafana/faro-core';
2
+
3
+ import { BaseCrashReportingInstrumentation } from '../BaseCrashReportingInstrumentation';
4
+ import type { CrashReport } from '../types';
5
+
6
+ import { resolveCrashErrorMessage, shouldSkipCrashReport } from './crashErrorMessage';
7
+ import { parseAndroidCrashTrace, type ParsedAndroidCrashTrace } from './parseAndroidCrashTrace';
8
+
9
+ /**
10
+ * Android Crash Reporting Instrumentation.
11
+ *
12
+ * Uses ApplicationExitInfo API (Android 11+ / API 30+) to retrieve crash reports
13
+ * from previous sessions. Captures:
14
+ * - CRASH: Java/Kotlin exception crashes
15
+ * - CRASH_NATIVE: Native (NDK) crashes
16
+ * - LOW_MEMORY: App killed due to low memory
17
+ * - EXCESSIVE_RESOURCE_USAGE: App killed due to excessive resource usage
18
+ *
19
+ * ANRs are reported separately by ANRInstrumentation, not via ApplicationExitInfo replay.
20
+ */
21
+ export class AndroidCrashReportingInstrumentation extends BaseCrashReportingInstrumentation {
22
+ readonly name = '@grafana/faro-react-native:instrumentation-crash-android';
23
+ readonly version = VERSION;
24
+
25
+ protected parseCrashTrace(trace: string): ParsedAndroidCrashTrace | null {
26
+ return parseAndroidCrashTrace(trace, {
27
+ releaseBundleFilename: this.options.releaseBundleFilename,
28
+ });
29
+ }
30
+
31
+ protected resolveCrashErrorMessage(crash: CrashReport, parsedTrace: ParsedAndroidCrashTrace | null): string {
32
+ return resolveCrashErrorMessage(crash, parsedTrace);
33
+ }
34
+
35
+ protected shouldSkipCrashReport(crash: CrashReport): boolean {
36
+ return shouldSkipCrashReport(crash);
37
+ }
38
+
39
+ protected getStackFrames(parsedTrace: ParsedAndroidCrashTrace | null): any[] {
40
+ if (!parsedTrace) {
41
+ return [];
42
+ }
43
+ // Android has JS frames, Java/Kotlin frames (R8 retrace), and NDK tombstone rows (.so retrace).
44
+ return [...(parsedTrace.jsFrames ?? []), ...(parsedTrace.frames ?? []), ...(parsedTrace.nativeFrames ?? [])];
45
+ }
46
+
47
+ protected buildPlatformContext(parsedTrace: ParsedAndroidCrashTrace | null, context: Record<string, string>): void {
48
+ // Store the native Java/Kotlin exception class separately for drill-down
49
+ // Plugin overview metrics key off exception_type = 'crash', so keep that stable
50
+ if (parsedTrace?.exceptionType) {
51
+ context['nativeExceptionType'] = parsedTrace.exceptionType;
52
+ }
53
+ }
54
+
55
+ protected getParseFailureWarning(): string {
56
+ return 'Native crash trace present but no frames parsed; check Android trace format or UncaughtExceptionHandler cache';
57
+ }
58
+
59
+ protected getTraceUnavailableWarning(): string {
60
+ return 'Native crash trace unavailable (ApplicationExitInfo traceInputStream was null at crash time)';
61
+ }
62
+ }
@@ -0,0 +1,182 @@
1
+ import type { CrashReport } from '../types';
2
+
3
+ import {
4
+ buildFallbackCrashMessage,
5
+ isAnrTimeoutDescription,
6
+ resolveCrashErrorMessage,
7
+ shouldSkipCrashReport,
8
+ shouldSkipLowSignalCrashReport,
9
+ } from './crashErrorMessage';
10
+ import type { ParsedAndroidCrashTrace } from './parseAndroidCrashTrace';
11
+
12
+ const baseCrash: CrashReport = {
13
+ reason: 'CRASH',
14
+ timestamp: 1_700_000_000_000,
15
+ status: 0,
16
+ };
17
+
18
+ describe('resolveCrashErrorMessage', () => {
19
+ it('prefers the parsed trace message', () => {
20
+ const parsed: ParsedAndroidCrashTrace = {
21
+ exceptionMessage: 'Intentional native crash from demo module',
22
+ exceptionType: 'java.lang.RuntimeException',
23
+ jsFrames: [],
24
+ frames: [],
25
+ };
26
+
27
+ expect(resolveCrashErrorMessage(baseCrash, parsed)).toBe('Intentional native crash from demo module');
28
+ });
29
+
30
+ it('uses ApplicationExitInfo description when the trace has no message', () => {
31
+ const crash: CrashReport = {
32
+ ...baseCrash,
33
+ description:
34
+ 'Input dispatching timed out (com.example.app/.MainActivity is not responding. Waited 5002ms for FocusEvent)',
35
+ };
36
+
37
+ expect(resolveCrashErrorMessage(crash, null)).toBe(
38
+ 'Input dispatching timed out (com.example.app/.MainActivity is not responding. Waited 5002ms for FocusEvent)'
39
+ );
40
+ });
41
+
42
+ it('extracts java exception types from non-generic ApplicationExitInfo descriptions', () => {
43
+ const crash: CrashReport = {
44
+ ...baseCrash,
45
+ description: 'java.lang.NullPointerException: null reference',
46
+ };
47
+
48
+ expect(resolveCrashErrorMessage(crash, null)).toBe('java.lang.NullPointerException');
49
+ });
50
+
51
+ it('prefers parsed exception type over generic ApplicationExitInfo descriptions such as "crash"', () => {
52
+ const parsed: ParsedAndroidCrashTrace = {
53
+ exceptionType: 'java.lang.NullPointerException',
54
+ jsFrames: [],
55
+ frames: [],
56
+ };
57
+ const crash: CrashReport = {
58
+ ...baseCrash,
59
+ description: 'crash',
60
+ };
61
+
62
+ expect(resolveCrashErrorMessage(crash, parsed)).toBe('java.lang.NullPointerException');
63
+ });
64
+
65
+ it('falls back to exception type for message-less traces such as NullPointerException', () => {
66
+ const parsed: ParsedAndroidCrashTrace = {
67
+ exceptionType: 'java.lang.NullPointerException',
68
+ jsFrames: [],
69
+ frames: [],
70
+ };
71
+
72
+ expect(resolveCrashErrorMessage(baseCrash, parsed)).toBe('java.lang.NullPointerException');
73
+ });
74
+
75
+ it('uses generic fallback when nothing else is available', () => {
76
+ expect(resolveCrashErrorMessage(baseCrash, null)).toBe('CRASH: Application crash (Java/Kotlin), status: 0');
77
+ });
78
+
79
+ it('does not treat version-like tokens as exception types', () => {
80
+ const parsed: ParsedAndroidCrashTrace = {
81
+ exceptionType: '18.2213',
82
+ jsFrames: [],
83
+ frames: [
84
+ {
85
+ module: 'com.example.CrashModule',
86
+ function: 'blockMainThread',
87
+ filename: 'CrashModule.kt',
88
+ lineno: 56,
89
+ },
90
+ ],
91
+ };
92
+
93
+ expect(resolveCrashErrorMessage(baseCrash, parsed)).toBe('CRASH: Application crash (Java/Kotlin), status: 0');
94
+ });
95
+
96
+ it('falls back when the trace has frames but no parseable message or exception type', () => {
97
+ const parsed: ParsedAndroidCrashTrace = {
98
+ jsFrames: [],
99
+ frames: [
100
+ {
101
+ module: 'com.example.CrashModule',
102
+ function: 'triggerNativeCrash',
103
+ filename: 'CrashModule.kt',
104
+ lineno: 60,
105
+ },
106
+ ],
107
+ };
108
+
109
+ expect(resolveCrashErrorMessage(baseCrash, parsed)).toBe('CRASH: Application crash (Java/Kotlin), status: 0');
110
+ });
111
+ });
112
+
113
+ describe('isAnrTimeoutDescription', () => {
114
+ it('detects Android ANR watchdog descriptions', () => {
115
+ expect(
116
+ isAnrTimeoutDescription(
117
+ 'Input dispatching timed out (com.example.app/com.example.app.MainActivity is not responding. Waited 5002ms for FocusEvent)'
118
+ )
119
+ ).toBe(true);
120
+ expect(isAnrTimeoutDescription('Application Not Responding')).toBe(true);
121
+ expect(isAnrTimeoutDescription('java.lang.RuntimeException: something broke')).toBe(false);
122
+ });
123
+ });
124
+
125
+ describe('shouldSkipCrashReport', () => {
126
+ it('skips ApplicationExitInfo rows with ANR timeout descriptions', () => {
127
+ expect(
128
+ shouldSkipCrashReport({
129
+ ...baseCrash,
130
+ description:
131
+ 'Input dispatching timed out (com.example.app/com.example.app.MainActivity is not responding. Waited 5002ms for FocusEvent)',
132
+ })
133
+ ).toBe(true);
134
+ });
135
+
136
+ it('does not skip real crashes with Java exception descriptions', () => {
137
+ expect(
138
+ shouldSkipCrashReport({
139
+ ...baseCrash,
140
+ description: 'java.lang.RuntimeException: Network failure',
141
+ trace: 'java.lang.RuntimeException: Network failure\n at com.example.Foo.bar(Foo.kt:1)',
142
+ })
143
+ ).toBe(false);
144
+ });
145
+ });
146
+
147
+ describe('shouldSkipLowSignalCrashReport', () => {
148
+ it('skips trace-less rows with only generic descriptions', () => {
149
+ expect(
150
+ shouldSkipLowSignalCrashReport({
151
+ ...baseCrash,
152
+ description: 'Application crash (Java/Kotlin)',
153
+ })
154
+ ).toBe(true);
155
+ });
156
+
157
+ it('keeps rows that carry a native trace', () => {
158
+ expect(
159
+ shouldSkipLowSignalCrashReport({
160
+ ...baseCrash,
161
+ trace: 'java.lang.NullPointerException\n at com.example.Foo.bar(Foo.kt:1)',
162
+ })
163
+ ).toBe(false);
164
+ });
165
+
166
+ it('keeps rows with a specific ApplicationExitInfo description', () => {
167
+ expect(
168
+ shouldSkipLowSignalCrashReport({
169
+ ...baseCrash,
170
+ description: 'java.lang.RuntimeException: intentional native crash',
171
+ })
172
+ ).toBe(false);
173
+ });
174
+ });
175
+
176
+ describe('buildFallbackCrashMessage', () => {
177
+ it('formats native crash reasons', () => {
178
+ expect(buildFallbackCrashMessage({ ...baseCrash, reason: 'CRASH_NATIVE' })).toBe(
179
+ 'CRASH_NATIVE: Application crash (Native), status: 0'
180
+ );
181
+ });
182
+ });