@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
@@ -0,0 +1,135 @@
1
+ import type { CrashReport } from '../types';
2
+
3
+ import { isPlausibleJavaExceptionIdentifier } from './parseAndroidCrashTrace';
4
+ import type { ParsedAndroidCrashTrace } from './parseAndroidCrashTrace';
5
+
6
+ /**
7
+ * Human-readable exception value for ApplicationExitInfo crash reports.
8
+ * Priority: parsed trace message → exception class → description → generic fallback.
9
+ */
10
+ export function resolveCrashErrorMessage(crash: CrashReport, parsedTrace: ParsedAndroidCrashTrace | null): string {
11
+ const fromTrace = parsedTrace?.exceptionMessage?.trim();
12
+ if (fromTrace) {
13
+ return fromTrace;
14
+ }
15
+
16
+ const fromType = parsedTrace?.exceptionType?.trim();
17
+ if (fromType && isPlausibleJavaExceptionIdentifier(fromType)) {
18
+ return fromType;
19
+ }
20
+
21
+ const fromDescriptionType = exceptionTypeFromDescription(crash.description);
22
+ if (fromDescriptionType) {
23
+ return fromDescriptionType;
24
+ }
25
+
26
+ const fromDescription = crash.description?.trim();
27
+ if (fromDescription && !isGenericCrashDescription(fromDescription)) {
28
+ return fromDescription;
29
+ }
30
+
31
+ const fallback = buildFallbackCrashMessage(crash);
32
+ return fallback.trim() || crash.reason || 'Application crash';
33
+ }
34
+
35
+ const ANR_TIMEOUT_DESCRIPTION_PATTERN = /input dispatching timed out|not responding|application not responding/i;
36
+
37
+ /**
38
+ * Android ANR watchdog descriptions from ApplicationExitInfo (any app).
39
+ */
40
+ export function isAnrTimeoutDescription(description: string | undefined): boolean {
41
+ const trimmed = description?.trim();
42
+ if (!trimmed) {
43
+ return false;
44
+ }
45
+ return ANR_TIMEOUT_DESCRIPTION_PATTERN.test(trimmed);
46
+ }
47
+
48
+ /**
49
+ * Skip duplicate ApplicationExitInfo rows that have no stack and no useful title,
50
+ * and ANR incidents mis-tagged as previous-session crashes.
51
+ */
52
+ export function shouldSkipCrashReport(crash: CrashReport): boolean {
53
+ if (isAnrTimeoutDescription(crash.description)) {
54
+ return true;
55
+ }
56
+
57
+ return shouldSkipLowSignalCrashReport(crash);
58
+ }
59
+
60
+ /**
61
+ * Skip duplicate ApplicationExitInfo rows that have no stack and no useful title.
62
+ * These surface in the plugin as a second generic `crash` row for a single button press.
63
+ */
64
+ export function shouldSkipLowSignalCrashReport(crash: CrashReport): boolean {
65
+ if (crash.trace?.trim()) {
66
+ return false;
67
+ }
68
+
69
+ const description = crash.description?.trim();
70
+ if (description && !isGenericCrashDescription(description)) {
71
+ return false;
72
+ }
73
+
74
+ return true;
75
+ }
76
+
77
+ /** Extract `java.lang.NullPointerException` from ApplicationExitInfo description text. */
78
+ function exceptionTypeFromDescription(description: string | undefined): string | undefined {
79
+ const trimmed = description?.trim();
80
+ if (!trimmed || isGenericCrashDescription(trimmed)) {
81
+ return undefined;
82
+ }
83
+
84
+ const match = trimmed.match(/^([\w.$]+)(?::(.*))?$/);
85
+ const candidate = match?.[1]?.trim();
86
+ if (!candidate || !isPlausibleJavaExceptionIdentifier(candidate)) {
87
+ return undefined;
88
+ }
89
+
90
+ return candidate;
91
+ }
92
+
93
+ function isGenericCrashDescription(description: string): boolean {
94
+ const normalized = description.trim().toLowerCase();
95
+ return (
96
+ normalized === 'crash' ||
97
+ normalized === 'native crash' ||
98
+ normalized === 'application crash' ||
99
+ normalized === 'application crash (java/kotlin)' ||
100
+ normalized === 'application crash (native)'
101
+ );
102
+ }
103
+
104
+ /** Generic fallback when the trace carries no parseable message or type. */
105
+ export function buildFallbackCrashMessage(crash: CrashReport): string {
106
+ const reason = crash.reason || 'UNKNOWN';
107
+ const status = crash.status ?? 0;
108
+
109
+ let description: string;
110
+ switch (crash.reason) {
111
+ case 'ANR':
112
+ description = 'Application Not Responding';
113
+ break;
114
+ case 'CRASH':
115
+ description = 'Application crash (Java/Kotlin)';
116
+ break;
117
+ case 'CRASH_NATIVE':
118
+ description = 'Application crash (Native)';
119
+ break;
120
+ case 'LOW_MEMORY':
121
+ description = 'Application terminated due to low memory';
122
+ break;
123
+ case 'EXCESSIVE_RESOURCE_USAGE':
124
+ description = 'Application terminated due to excessive resource usage';
125
+ break;
126
+ case 'INITIALIZATION_FAILURE':
127
+ description = 'Application failed to initialize';
128
+ break;
129
+ default:
130
+ description = crash.description || 'Application crash';
131
+ break;
132
+ }
133
+
134
+ return `${reason}: ${description}, status: ${status}`;
135
+ }
@@ -0,0 +1,4 @@
1
+ export { AndroidCrashReportingInstrumentation } from './AndroidCrashReportingInstrumentation';
2
+ export { parseAndroidCrashTrace, normalizeJavaStackTraceForRetrace } from './parseAndroidCrashTrace';
3
+ export type { ParsedAndroidCrashTrace, AndroidCrashStackFrame } from './parseAndroidCrashTrace';
4
+ export { resolveCrashErrorMessage, shouldSkipCrashReport } from './crashErrorMessage';
@@ -0,0 +1,19 @@
1
+ import { looksLikeNativeTombstoneTrace } from './nativeTombstoneTrace';
2
+
3
+ describe('looksLikeNativeTombstoneTrace', () => {
4
+ it('detects Android tombstone backtrace lines', () => {
5
+ const trace = [
6
+ '*** *** *** *** *** *** ***',
7
+ "ABI: 'arm64'",
8
+ 'backtrace:',
9
+ ' #00 pc 0000000000001234 /data/app/lib/arm64-v8a/libappmodules.so (nativeCrash+8)',
10
+ ].join('\n');
11
+
12
+ expect(looksLikeNativeTombstoneTrace(trace)).toBe(true);
13
+ });
14
+
15
+ it('returns false for Java stack traces', () => {
16
+ const trace = ['java.lang.RuntimeException: boom', ' at com.example.Foo.bar(Foo.kt:12)'].join('\n');
17
+ expect(looksLikeNativeTombstoneTrace(trace)).toBe(false);
18
+ });
19
+ });
@@ -0,0 +1,8 @@
1
+ /** Detect Android tombstone backtrace lines used for NDK native retrace. */
2
+ export function looksLikeNativeTombstoneTrace(trace: string | undefined): boolean {
3
+ if (!trace?.trim()) {
4
+ return false;
5
+ }
6
+
7
+ return trace.split('\n').some((line) => /^\s*#\d+\s+pc\s+(?:0x)?[0-9a-fA-F]+\s+\S+/i.test(line));
8
+ }
@@ -0,0 +1,232 @@
1
+ import {
2
+ normalizeCrashTraceExceptionMessage,
3
+ normalizeJavaStackTraceForRetrace,
4
+ parseAndroidCrashTrace,
5
+ } from './parseAndroidCrashTrace';
6
+
7
+ describe('parseAndroidCrashTrace', () => {
8
+ it('parses obfuscated ApplicationExitInfo traces', () => {
9
+ const trace = [
10
+ 'com.example.c: Intentional native crash',
11
+ '\tat com.example.c.a(SourceFile:33)',
12
+ '\tat com.example.c.b(SourceFile:45)',
13
+ '\tat com.example.c.d(SourceFile)',
14
+ '\tat com.unknown.x.y(SourceFile:7)',
15
+ ].join('\n');
16
+
17
+ const parsed = parseAndroidCrashTrace(trace);
18
+
19
+ expect(parsed).toEqual({
20
+ exceptionType: 'com.example.c',
21
+ exceptionMessage: 'Intentional native crash',
22
+ jsFrames: [],
23
+ nativeFrames: [],
24
+ frames: [
25
+ {
26
+ module: 'com.example.c',
27
+ function: 'a',
28
+ filename: 'SourceFile',
29
+ lineno: 33,
30
+ },
31
+ {
32
+ module: 'com.example.c',
33
+ function: 'b',
34
+ filename: 'SourceFile',
35
+ lineno: 45,
36
+ },
37
+ {
38
+ module: 'com.example.c',
39
+ function: 'd',
40
+ filename: 'SourceFile',
41
+ lineno: undefined,
42
+ },
43
+ {
44
+ module: 'com.unknown.x',
45
+ function: 'y',
46
+ filename: 'SourceFile',
47
+ lineno: 7,
48
+ },
49
+ ],
50
+ });
51
+ });
52
+
53
+ it('parses Android Log.getStackTraceString format', () => {
54
+ const trace = [
55
+ 'java.lang.RuntimeException: Intentional native crash',
56
+ ' at com.example.CrashModule.raiseFailure(CrashModule.kt:60)',
57
+ ' at com.example.CrashModule.triggerRuntimeException(CrashModule.kt:33)',
58
+ ].join('\n');
59
+
60
+ const parsed = parseAndroidCrashTrace(trace);
61
+
62
+ expect(parsed?.exceptionType).toBe('java.lang.RuntimeException');
63
+ expect(parsed?.frames).toEqual([
64
+ {
65
+ module: 'com.example.CrashModule',
66
+ function: 'raiseFailure',
67
+ filename: 'CrashModule.kt',
68
+ lineno: 60,
69
+ },
70
+ {
71
+ module: 'com.example.CrashModule',
72
+ function: 'triggerRuntimeException',
73
+ filename: 'CrashModule.kt',
74
+ lineno: 33,
75
+ },
76
+ ]);
77
+ });
78
+
79
+ it('parses java.lang.RuntimeException headers', () => {
80
+ const trace = ['java.lang.RuntimeException: Intentional native crash', '\tat com.example.c.a(SourceFile:33)'].join(
81
+ '\n'
82
+ );
83
+
84
+ const parsed = parseAndroidCrashTrace(trace);
85
+
86
+ expect(parsed?.exceptionType).toBe('java.lang.RuntimeException');
87
+ expect(parsed?.frames).toHaveLength(1);
88
+ });
89
+
90
+ it('returns null for empty input', () => {
91
+ expect(parseAndroidCrashTrace('')).toBeNull();
92
+ expect(parseAndroidCrashTrace(' ')).toBeNull();
93
+ });
94
+
95
+ it('parses RN fatal JavascriptException ApplicationExitInfo traces', () => {
96
+ const trace = [
97
+ 'com.facebook.react.common.JavascriptException: Error: Unhandled debug exception, stack:',
98
+ 'anonymous@1:1390953',
99
+ '',
100
+ '\tat com.facebook.react.modules.core.ExceptionsManagerModule.reportException(ExceptionsManagerModule.kt:52)',
101
+ '\tat android.os.Handler.handleCallback(Handler.java:1070)',
102
+ ].join('\n');
103
+
104
+ const parsed = parseAndroidCrashTrace(trace, { releaseBundleFilename: 'index.android.bundle' });
105
+
106
+ expect(parsed?.exceptionType).toBe('com.facebook.react.common.JavascriptException');
107
+ expect(parsed?.exceptionMessage).toBe('Unhandled debug exception');
108
+ expect(parsed?.jsFrames).toEqual([
109
+ {
110
+ function: 'anonymous',
111
+ filename: 'index.android.bundle',
112
+ lineno: 1,
113
+ colno: 1390953,
114
+ },
115
+ ]);
116
+ expect(parsed?.frames[0]).toMatchObject({
117
+ module: 'com.facebook.react.modules.core.ExceptionsManagerModule',
118
+ function: 'reportException',
119
+ filename: 'ExceptionsManagerModule.kt',
120
+ lineno: 52,
121
+ });
122
+ });
123
+
124
+ it('parses dev-style JS frames embedded in Android crash traces', () => {
125
+ const trace = [
126
+ 'com.facebook.react.common.JavascriptException: Error: Unhandled debug exception, stack:',
127
+ 'at triggerUnhandledException (/Users/me/DebugScreen.tsx:133:7)',
128
+ 'at com.facebook.react.modules.core.ExceptionsManagerModule.reportException(ExceptionsManagerModule.kt:52)',
129
+ ].join('\n');
130
+
131
+ const parsed = parseAndroidCrashTrace(trace);
132
+
133
+ expect(parsed?.jsFrames[0]).toMatchObject({
134
+ function: 'triggerUnhandledException',
135
+ filename: '/Users/me/DebugScreen.tsx',
136
+ lineno: 133,
137
+ colno: 7,
138
+ });
139
+ });
140
+
141
+ it('normalizes RN fatal JS error header shape without relying on exception type', () => {
142
+ expect(normalizeCrashTraceExceptionMessage('Error: Unhandled debug exception, stack:')).toBe(
143
+ 'Unhandled debug exception'
144
+ );
145
+ });
146
+
147
+ it('leaves native exception messages unchanged', () => {
148
+ expect(normalizeCrashTraceExceptionMessage('Intentional native crash')).toBe('Intentional native crash');
149
+ });
150
+
151
+ it('parses ANRTracker Thread.getStackTrace lines without an "at " prefix', () => {
152
+ const trace = [
153
+ 'com.example.CrashModule.blockMainThread(CrashModule.kt:42)',
154
+ 'com.example.CrashModule.crash(CrashModule.kt:22)',
155
+ ].join('\n');
156
+
157
+ const parsed = parseAndroidCrashTrace(trace);
158
+
159
+ expect(parsed?.frames).toEqual([
160
+ {
161
+ module: 'com.example.CrashModule',
162
+ function: 'blockMainThread',
163
+ filename: 'CrashModule.kt',
164
+ lineno: 42,
165
+ },
166
+ {
167
+ module: 'com.example.CrashModule',
168
+ function: 'crash',
169
+ filename: 'CrashModule.kt',
170
+ lineno: 22,
171
+ },
172
+ ]);
173
+ });
174
+
175
+ it('normalizes thread stacks for ingest R8 retrace', () => {
176
+ const raw = 'com.example.c.a(SourceFile:33)';
177
+ expect(normalizeJavaStackTraceForRetrace(raw)).toBe(' at com.example.c.a(SourceFile:33)');
178
+ });
179
+
180
+ it('ignores version-like header lines such as 18.2213', () => {
181
+ const trace = ['18.2213', ' at com.example.CrashModule.blockMainThread(CrashModule.kt:56)'].join('\n');
182
+
183
+ const parsed = parseAndroidCrashTrace(trace);
184
+
185
+ expect(parsed?.exceptionType).toBeUndefined();
186
+ expect(parsed?.frames).toHaveLength(1);
187
+ });
188
+
189
+ it('parses NDK tombstone backtrace lines and ignores the backtrace: section label', () => {
190
+ const trace = [
191
+ "ABI: 'arm64'",
192
+ 'signal SIGSEGV (11)',
193
+ 'backtrace:',
194
+ ' #00 pc 000000782a48ce5c /data/app/base.apk!libappmodules.so (Java_com_quickpizza_QuickPizzaNdkCrashModule_nativeCrash+8)',
195
+ ' #01 pc 0000000070ae9890 /system/framework/arm64/boot.oat (art_jni_trampoline+112)',
196
+ ].join('\n');
197
+
198
+ const parsed = parseAndroidCrashTrace(trace);
199
+
200
+ expect(parsed?.exceptionType).toBeUndefined();
201
+ expect(parsed?.nativeFrames).toEqual([
202
+ {
203
+ module: 'libappmodules.so',
204
+ function: 'Java_com_quickpizza_QuickPizzaNdkCrashModule_nativeCrash',
205
+ filename: 'libappmodules.so',
206
+ },
207
+ {
208
+ module: 'boot.oat',
209
+ function: 'art_jni_trampoline',
210
+ filename: 'boot.oat',
211
+ },
212
+ ]);
213
+ });
214
+
215
+ it('captures root cause from wrapped exceptions with "Caused by:" chain', () => {
216
+ const trace = [
217
+ 'java.lang.RuntimeException: Wrapper exception',
218
+ ' at com.example.OuterClass.method(OuterClass.kt:10)',
219
+ 'Caused by: java.lang.IllegalStateException: Middle exception',
220
+ ' at com.example.MiddleClass.method(MiddleClass.kt:20)',
221
+ 'Caused by: java.lang.NullPointerException: Root cause',
222
+ ' at com.example.InnerClass.method(InnerClass.kt:30)',
223
+ ].join('\n');
224
+
225
+ const parsed = parseAndroidCrashTrace(trace);
226
+
227
+ // Should capture the root cause (last "Caused by:")
228
+ expect(parsed?.exceptionType).toBe('java.lang.NullPointerException');
229
+ expect(parsed?.exceptionMessage).toBe('Root cause');
230
+ expect(parsed?.frames).toHaveLength(3);
231
+ });
232
+ });
@@ -0,0 +1,235 @@
1
+ import type { ExceptionStackFrame } from '@grafana/faro-core';
2
+
3
+ import { parseStackTraceLine, toFaroStackFrames } from '../../errors/stackTraceParser';
4
+
5
+ /**
6
+ * Android ApplicationExitInfo traces use Java/Kotlin stack-trace formatting.
7
+ * Faro ingest expects structured frames with `module` (obfuscated class) so the
8
+ * collector can R8-retrace them — not the JS call stack from sendCrashReport().
9
+ *
10
+ * Fatal RN JS errors also embed a short Hermes/Metro stack before native frames
11
+ * (e.g. `anonymous@1:1390953`). Those lines are parsed as JS frames (no `module`)
12
+ * so the collector can source-map them to files like DebugScreen.tsx.
13
+ */
14
+ export interface AndroidCrashStackFrame extends ExceptionStackFrame {
15
+ module: string;
16
+ }
17
+
18
+ export interface ParseAndroidCrashTraceOptions {
19
+ releaseBundleFilename?: string;
20
+ }
21
+
22
+ export interface NativeTombstoneStackFrame extends ExceptionStackFrame {
23
+ /** Unstripped .so basename (e.g. libappmodules.so) for NDK retrace on the collector. */
24
+ module: string;
25
+ }
26
+
27
+ export interface ParsedAndroidCrashTrace {
28
+ exceptionType?: string;
29
+ /** Human-readable message from the trace header (e.g. the JS error text). */
30
+ exceptionMessage?: string;
31
+ /** JS/Hermes frames from the embedded RN stack (source-map targets). */
32
+ jsFrames: ExceptionStackFrame[];
33
+ /** Native Java/Kotlin frames (Android R8 retrace targets). */
34
+ frames: AndroidCrashStackFrame[];
35
+ /** NDK tombstone #NN pc lines (server-side .so retrace targets). */
36
+ nativeFrames: NativeTombstoneStackFrame[];
37
+ }
38
+
39
+ // Mirrors pkg/exporter/androidretrace/mapping.go frameLine / exceptionHeader.
40
+ const FRAME_LINE = /^\s*at\s+([\w$.]+)\.([\w$<>]+)\(([^):]*)(?::(\d+))?\)\s*$/;
41
+ // Thread.getStackTrace() / legacy ANRTracker lines without the "at " prefix.
42
+ const THREAD_STACK_FRAME_LINE = /^\s*([\w$.]+)\.([\w$<>]+)\(([^):]*)(?::(\d+))?\)\s*$/;
43
+ const EXCEPTION_HEADER = /^\s*(?:Caused by:\s*)?([\w$.]+)(?::(.*))?$/;
44
+ // Mirrors pkg/exporter/androidretrace/native_frame.go nativeFrameLine.
45
+ const NATIVE_TOMBSTONE_FRAME_LINE = /^\s*#(\d+)\s+pc\s+(?:0x)?([0-9a-fA-F]+)\s+(\S+)(?:\s+\(([^)]+)\))?/i;
46
+
47
+ const TOMBSTONE_SECTION_LABELS = new Set(['backtrace', 'stack', 'build id', 'memory map', 'memory near', 'abi']);
48
+
49
+ /** Tombstone section headers and metadata must not become exception_type / nativeExceptionType. */
50
+ export function isTombstoneSectionLabel(value: string): boolean {
51
+ return TOMBSTONE_SECTION_LABELS.has(value.trim().toLowerCase());
52
+ }
53
+
54
+ function isTombstoneMetadataLine(line: string): boolean {
55
+ const trimmed = line.trim();
56
+ if (!trimmed) {
57
+ return true;
58
+ }
59
+ if (trimmed.startsWith('***')) {
60
+ return true;
61
+ }
62
+ if (/^ABI:/i.test(trimmed)) {
63
+ return true;
64
+ }
65
+ if (/^signal\s+/i.test(trimmed)) {
66
+ return true;
67
+ }
68
+ if (/^pid:/i.test(trimmed)) {
69
+ return true;
70
+ }
71
+ if (isTombstoneSectionLabel(trimmed.replace(/:$/, ''))) {
72
+ return true;
73
+ }
74
+ return false;
75
+ }
76
+
77
+ function nativeLibBasename(token: string): string {
78
+ const bang = token.lastIndexOf('!');
79
+ if (bang >= 0) {
80
+ token = token.slice(bang + 1);
81
+ }
82
+ const slash = token.lastIndexOf('/');
83
+ return slash >= 0 ? token.slice(slash + 1) : token;
84
+ }
85
+
86
+ function parseNativeTombstoneFrameLine(line: string): NativeTombstoneStackFrame | null {
87
+ const match = line.match(NATIVE_TOMBSTONE_FRAME_LINE);
88
+ if (!match) {
89
+ return null;
90
+ }
91
+
92
+ const library = match[3] ?? '';
93
+ const suffix = match[4]?.trim();
94
+ let fn = '<unknown>';
95
+ if (suffix) {
96
+ const fnMatch = suffix.match(/^([A-Za-z_][A-Za-z0-9_]*)(?:\+(\d+))?$/);
97
+ if (fnMatch?.[1] && fnMatch[1] !== 'BuildId' && fnMatch[1] !== 'offset') {
98
+ fn = fnMatch[1];
99
+ }
100
+ }
101
+
102
+ return {
103
+ module: nativeLibBasename(library),
104
+ function: fn,
105
+ filename: nativeLibBasename(library),
106
+ };
107
+ }
108
+
109
+ /** Reject version tokens (e.g. "18.2213") and other non-class header lines. */
110
+ export function isPlausibleJavaExceptionIdentifier(value: string): boolean {
111
+ const trimmed = value.trim();
112
+ if (!trimmed || !/[a-zA-Z]/.test(trimmed)) {
113
+ return false;
114
+ }
115
+ if (/^\d+(\.\d+)+$/.test(trimmed)) {
116
+ return false;
117
+ }
118
+ return true;
119
+ }
120
+
121
+ /**
122
+ * Normalize raw Java thread stacks (e.g. ANRTracker) to Log.getStackTraceString shape
123
+ * so collector R8 retrace (mapping.RetraceText) can rewrite frame lines.
124
+ */
125
+ export function normalizeJavaStackTraceForRetrace(trace: string): string {
126
+ return trace
127
+ .split('\n')
128
+ .map((line) => {
129
+ const trimmed = line.trim();
130
+ if (!trimmed) {
131
+ return line;
132
+ }
133
+ if (/^\s*at\s+/.test(line)) {
134
+ return line;
135
+ }
136
+ if (THREAD_STACK_FRAME_LINE.test(trimmed)) {
137
+ return ` at ${trimmed}`;
138
+ }
139
+ return line;
140
+ })
141
+ .join('\n');
142
+ }
143
+
144
+ /**
145
+ * Normalize exception text from ApplicationExitInfo / Log.getStackTraceString headers.
146
+ *
147
+ * React Native fatal JS errors embed the JS message in a fixed header shape
148
+ * (`Error: <message>, stack:`) regardless of the wrapping Java exception class.
149
+ * Match on that shape instead of the exception type name.
150
+ */
151
+ const RN_FATAL_JS_ERROR_HEADER = /^Error:\s*(.+?),\s*stack:\s*$/i;
152
+
153
+ export function normalizeCrashTraceExceptionMessage(rawMessage: string | undefined): string | undefined {
154
+ if (!rawMessage?.trim()) {
155
+ return undefined;
156
+ }
157
+
158
+ const message = rawMessage.trim();
159
+ const rnFatalMatch = message.match(RN_FATAL_JS_ERROR_HEADER);
160
+
161
+ return (rnFatalMatch?.[1] ?? message).trim() || undefined;
162
+ }
163
+
164
+ function parseJsFrameLine(line: string, options?: ParseAndroidCrashTraceOptions): ExceptionStackFrame | null {
165
+ const parsed = parseStackTraceLine(line);
166
+ if (!parsed) {
167
+ return null;
168
+ }
169
+
170
+ return toFaroStackFrames([parsed], options)[0] ?? null;
171
+ }
172
+
173
+ /**
174
+ * Parse a raw Android crash trace into structured stack frames for pushError().
175
+ */
176
+ export function parseAndroidCrashTrace(
177
+ trace: string,
178
+ options?: ParseAndroidCrashTraceOptions
179
+ ): ParsedAndroidCrashTrace | null {
180
+ const trimmed = trace.trim();
181
+ if (!trimmed) {
182
+ return null;
183
+ }
184
+
185
+ const lines = trimmed.split('\n');
186
+ const jsFrames: ExceptionStackFrame[] = [];
187
+ const frames: AndroidCrashStackFrame[] = [];
188
+ const nativeFrames: NativeTombstoneStackFrame[] = [];
189
+ let exceptionType: string | undefined;
190
+ let exceptionMessage: string | undefined;
191
+
192
+ lines.forEach((line) => {
193
+ const nativeFrame = parseNativeTombstoneFrameLine(line);
194
+ if (nativeFrame) {
195
+ nativeFrames.push(nativeFrame);
196
+ return;
197
+ }
198
+
199
+ const frameMatch = line.match(FRAME_LINE) ?? line.trim().match(THREAD_STACK_FRAME_LINE);
200
+
201
+ if (frameMatch) {
202
+ const [, module, method, filename, lineNo] = frameMatch;
203
+ frames.push({
204
+ module: module ?? '',
205
+ function: method ?? '<unknown>',
206
+ filename: filename?.trim() || 'SourceFile',
207
+ lineno: lineNo ? parseInt(lineNo, 10) : undefined,
208
+ });
209
+ return;
210
+ }
211
+
212
+ const jsFrame = parseJsFrameLine(line, options);
213
+ if (jsFrame) {
214
+ jsFrames.push(jsFrame);
215
+ return;
216
+ }
217
+
218
+ const headerMatch = line.match(EXCEPTION_HEADER);
219
+ if (
220
+ !isTombstoneMetadataLine(line) &&
221
+ headerMatch?.[1] &&
222
+ isPlausibleJavaExceptionIdentifier(headerMatch[1]) &&
223
+ !isTombstoneSectionLabel(headerMatch[1])
224
+ ) {
225
+ exceptionType = headerMatch[1];
226
+ exceptionMessage = normalizeCrashTraceExceptionMessage(headerMatch[2]);
227
+ }
228
+ });
229
+
230
+ if (frames.length === 0 && jsFrames.length === 0 && nativeFrames.length === 0 && !exceptionType) {
231
+ return null;
232
+ }
233
+
234
+ return { exceptionType, exceptionMessage, jsFrames, frames, nativeFrames };
235
+ }
@@ -1,2 +1,43 @@
1
- export { CrashReportingInstrumentation } from './instrumentation';
2
- export type { CrashReport, CrashReportingOptions } from './types';
1
+ import { Platform } from 'react-native';
2
+
3
+ import { AndroidCrashReportingInstrumentation } from './android';
4
+ import { IosCrashReportingInstrumentation } from './ios';
5
+ import { NoOpCrashReportingInstrumentation } from './NoOpCrashReportingInstrumentation';
6
+
7
+ function resolveCrashReportingInstrumentation() {
8
+ if (Platform.OS === 'android') {
9
+ return AndroidCrashReportingInstrumentation;
10
+ }
11
+
12
+ if (Platform.OS === 'ios') {
13
+ // React Native reports both iPhone and iPadOS as "ios".
14
+ return IosCrashReportingInstrumentation;
15
+ }
16
+
17
+ return NoOpCrashReportingInstrumentation;
18
+ }
19
+
20
+ /**
21
+ * Platform-aware crash reporting instrumentation export.
22
+ *
23
+ * At module load time, determines the platform and exports the correct implementation:
24
+ * - Android: Uses ApplicationExitInfo API to retrieve crashes
25
+ * - iOS: Uses PLCrashReporter to retrieve crashes
26
+ * - Other: No-op stub (web, macOS, Windows, etc.)
27
+ *
28
+ * Usage:
29
+ * ```ts
30
+ * import { CrashReportingInstrumentation } from '@grafana/faro-react-native/instrumentation-crash';
31
+ *
32
+ * const faro = initializeFaro({
33
+ * // ...
34
+ * instrumentations: [
35
+ * new CrashReportingInstrumentation(),
36
+ * ],
37
+ * });
38
+ * ```
39
+ */
40
+ export const CrashReportingInstrumentation = resolveCrashReportingInstrumentation();
41
+
42
+ // Export public API types
43
+ export type { CrashReportingOptions } from './types';