@grafana/faro-react-native 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) 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/ios/faro-upload-composed-source-map.sh +8 -2
  171. package/package.json +5 -5
  172. package/src/config/getRNInstrumentations.ts +5 -1
  173. package/src/config/makeRNConfig.test.ts +59 -3
  174. package/src/config/makeRNConfig.ts +48 -9
  175. package/src/config/types.ts +2 -1
  176. package/src/generated/faroRNPackageMeta.ts +1 -1
  177. package/src/index.ts +2 -1
  178. package/src/initialize.test.ts +71 -15
  179. package/src/initialize.ts +11 -5
  180. package/src/instrumentations/anr/anrStack.test.ts +27 -0
  181. package/src/instrumentations/anr/anrStack.ts +19 -0
  182. package/src/instrumentations/anr/instrumentation.test.ts +45 -0
  183. package/src/instrumentations/anr/instrumentation.ts +301 -67
  184. package/src/instrumentations/anr/types.ts +7 -1
  185. package/src/instrumentations/console/index.ts +5 -1
  186. package/src/instrumentations/console/instrumentation.test.ts +5 -0
  187. package/src/instrumentations/crashReporting/{instrumentation.ts → BaseCrashReportingInstrumentation.ts} +119 -93
  188. package/src/instrumentations/crashReporting/NoOpCrashReportingInstrumentation.ts +23 -0
  189. package/src/instrumentations/crashReporting/android/AndroidCrashReportingInstrumentation.ts +62 -0
  190. package/src/instrumentations/crashReporting/android/crashErrorMessage.test.ts +182 -0
  191. package/src/instrumentations/crashReporting/android/crashErrorMessage.ts +135 -0
  192. package/src/instrumentations/crashReporting/android/index.ts +4 -0
  193. package/src/instrumentations/crashReporting/android/nativeTombstoneTrace.test.ts +19 -0
  194. package/src/instrumentations/crashReporting/android/nativeTombstoneTrace.ts +8 -0
  195. package/src/instrumentations/crashReporting/android/parseAndroidCrashTrace.test.ts +232 -0
  196. package/src/instrumentations/crashReporting/android/parseAndroidCrashTrace.ts +235 -0
  197. package/src/instrumentations/crashReporting/index.ts +43 -2
  198. package/src/instrumentations/crashReporting/instrumentation.test.ts +42 -0
  199. package/src/instrumentations/crashReporting/ios/IosCrashReportingInstrumentation.ts +62 -0
  200. package/src/instrumentations/crashReporting/ios/crashErrorMessage.test.ts +170 -0
  201. package/src/instrumentations/crashReporting/ios/crashErrorMessage.ts +111 -0
  202. package/src/instrumentations/crashReporting/ios/index.ts +4 -0
  203. package/src/instrumentations/crashReporting/ios/parseIosCrashTrace.test.ts +86 -0
  204. package/src/instrumentations/crashReporting/ios/parseIosCrashTrace.ts +83 -0
  205. package/src/instrumentations/crashReporting/types.ts +19 -58
  206. package/src/instrumentations/errors/index.ts +2 -1
  207. package/src/instrumentations/errors/instrumentation.test.ts +8 -2
  208. package/src/instrumentations/errors/stackTraceParser.test.ts +1 -1
  209. package/src/instrumentations/session/installationId.ts +43 -0
  210. package/src/instrumentations/session/sessionAttributes.test.ts +260 -30
  211. package/src/instrumentations/session/sessionAttributes.ts +112 -22
  212. package/src/metas/appBuildIdentity.test.ts +48 -0
  213. package/src/metas/appBuildIdentity.ts +50 -0
  214. package/src/testUtils/mockDeviceInfo.ts +5 -0
  215. package/src/testUtils/setupTests.ts +7 -0
  216. package/dist/cjs/instrumentations/crashReporting/instrumentation.js.map +0 -1
  217. package/dist/esm/instrumentations/crashReporting/instrumentation.js.map +0 -1
  218. package/dist/types/instrumentations/crashReporting/instrumentation.d.ts +0 -58
@@ -0,0 +1,42 @@
1
+ import { type ExceptionEvent, initializeFaro, type TransportItem } from '@grafana/faro-core';
2
+ import { mockConfig, MockTransport } from '@grafana/faro-test-utils';
3
+
4
+ import { CrashReportingInstrumentation } from './index';
5
+ import type { CrashReport } from './types';
6
+
7
+ describe('CrashReportingInstrumentation', () => {
8
+ it('should report native crashes as fatal exceptions', () => {
9
+ const transport = new MockTransport();
10
+ const instrumentation = new CrashReportingInstrumentation({ enabled: false });
11
+
12
+ initializeFaro(
13
+ mockConfig({
14
+ transports: [transport],
15
+ instrumentations: [instrumentation],
16
+ })
17
+ );
18
+
19
+ (
20
+ instrumentation as unknown as {
21
+ sendCrashReport: (crash: CrashReport) => void;
22
+ }
23
+ ).sendCrashReport({
24
+ reason: 'CRASH_NATIVE',
25
+ signal: 'SIGSEGV (11)',
26
+ timestamp: 1710000000000,
27
+ trace: [
28
+ "ABI: 'arm64'",
29
+ 'backtrace:',
30
+ ' #00 pc 0000000000001234 /data/app/lib/arm64-v8a/libappmodules.so (nativeCrash+8)',
31
+ ].join('\n'),
32
+ });
33
+
34
+ expect(transport.items).toHaveLength(1);
35
+ const item = transport.items[0] as TransportItem<ExceptionEvent>;
36
+ expect(item.payload.type).toBe('crash');
37
+ expect(item.payload.fatal).toBe(true);
38
+ expect(item.payload.context?.mechanism).toBe('crash');
39
+ expect(item.payload.context?.signal).toBe('SIGSEGV (11)');
40
+ expect(item.payload.context?.trace).toContain('#00 pc');
41
+ });
42
+ });
@@ -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 { type ParsedIosCrashTrace, parseIosCrashTrace } from './parseIosCrashTrace';
8
+
9
+ /**
10
+ * iOS Crash Reporting Instrumentation.
11
+ *
12
+ * Uses PLCrashReporter to capture native crashes from previous sessions:
13
+ * - SIGSEGV: Segmentation fault / invalid memory access
14
+ * - SIGABRT: Abnormal termination (e.g., assertion failure, uncaught exception)
15
+ * - SIGBUS: Bus error / alignment/hardware error
16
+ * - SIGILL: Illegal instruction
17
+ * - SIGFPE: Floating point exception
18
+ * - SIGTRAP: Breakpoint/debugger trap
19
+ * - Mach exceptions (EXC_BAD_ACCESS, EXC_CRASH, etc.)
20
+ *
21
+ * Stack frames are symbolicated on the native side before being surfaced.
22
+ */
23
+ export class IosCrashReportingInstrumentation extends BaseCrashReportingInstrumentation {
24
+ readonly name = '@grafana/faro-react-native:instrumentation-crash-ios';
25
+ readonly version = VERSION;
26
+
27
+ protected parseCrashTrace(trace: string): ParsedIosCrashTrace | null {
28
+ return parseIosCrashTrace(trace);
29
+ }
30
+
31
+ protected resolveCrashErrorMessage(crash: CrashReport, parsedTrace: ParsedIosCrashTrace | 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: ParsedIosCrashTrace | null): any[] {
40
+ return parsedTrace?.frames ?? [];
41
+ }
42
+
43
+ protected buildPlatformContext(parsedTrace: ParsedIosCrashTrace | null, context: Record<string, string>): void {
44
+ if (parsedTrace?.exceptionName) {
45
+ context['nativeExceptionName'] = parsedTrace.exceptionName;
46
+ }
47
+ if (parsedTrace?.signalName) {
48
+ context['signalName'] = parsedTrace.signalName;
49
+ }
50
+ if (parsedTrace?.signalDescription) {
51
+ context['signalDescription'] = parsedTrace.signalDescription;
52
+ }
53
+ }
54
+
55
+ protected getParseFailureWarning(): string {
56
+ return 'Native crash trace present but no frames parsed; check iOS PLCrashReporter format';
57
+ }
58
+
59
+ protected getTraceUnavailableWarning(): string {
60
+ return 'Native crash trace unavailable (PLCrashReporter trace was null at crash time)';
61
+ }
62
+ }
@@ -0,0 +1,170 @@
1
+ import type { CrashReport } from '../types';
2
+
3
+ import { buildFallbackCrashMessage, resolveCrashErrorMessage, shouldSkipCrashReport } from './crashErrorMessage';
4
+ import type { ParsedIosCrashTrace } from './parseIosCrashTrace';
5
+
6
+ const baseCrash: CrashReport = {
7
+ reason: 'SIGSEGV',
8
+ timestamp: 1_700_000_000_000,
9
+ status: 0,
10
+ };
11
+
12
+ describe('resolveCrashErrorMessage', () => {
13
+ it('prefers exception name and reason for uncaught exceptions', () => {
14
+ const parsed: ParsedIosCrashTrace = {
15
+ exceptionName: 'NSInternalInconsistencyException',
16
+ exceptionReason: 'unable to dequeue a cell with identifier Cell',
17
+ frames: [],
18
+ };
19
+
20
+ expect(resolveCrashErrorMessage(baseCrash, parsed)).toBe(
21
+ 'NSInternalInconsistencyException: unable to dequeue a cell with identifier Cell'
22
+ );
23
+ });
24
+
25
+ it('uses exception name alone if no reason', () => {
26
+ const parsed: ParsedIosCrashTrace = {
27
+ exceptionName: 'NSRangeException',
28
+ frames: [],
29
+ };
30
+
31
+ expect(resolveCrashErrorMessage(baseCrash, parsed)).toBe('NSRangeException');
32
+ });
33
+
34
+ it('uses signal description from parsed trace', () => {
35
+ const parsed: ParsedIosCrashTrace = {
36
+ signalDescription: 'Segmentation fault',
37
+ frames: [],
38
+ };
39
+
40
+ expect(resolveCrashErrorMessage(baseCrash, parsed)).toBe('Segmentation fault');
41
+ });
42
+
43
+ it('uses signal name from parsed trace', () => {
44
+ const parsed: ParsedIosCrashTrace = {
45
+ signalName: 'SIGSEGV',
46
+ frames: [],
47
+ };
48
+
49
+ expect(resolveCrashErrorMessage(baseCrash, parsed)).toBe('Segmentation fault');
50
+ });
51
+
52
+ it('maps known signal names to descriptions', () => {
53
+ const parsed: ParsedIosCrashTrace = {
54
+ signalName: 'SIGABRT',
55
+ frames: [],
56
+ };
57
+
58
+ expect(resolveCrashErrorMessage(baseCrash, parsed)).toBe('Abort trap');
59
+ });
60
+
61
+ it('uses signal name as-is if unknown', () => {
62
+ const parsed: ParsedIosCrashTrace = {
63
+ signalName: 'SIGUNKNOWN',
64
+ frames: [],
65
+ };
66
+
67
+ expect(resolveCrashErrorMessage(baseCrash, parsed)).toBe('SIGUNKNOWN');
68
+ });
69
+
70
+ it('falls back to crash description if present', () => {
71
+ expect(resolveCrashErrorMessage({ ...baseCrash, description: 'Fatal error' }, null)).toBe('Fatal error');
72
+ });
73
+
74
+ it('uses crash reason with signal mapping', () => {
75
+ expect(resolveCrashErrorMessage({ ...baseCrash, reason: 'SIGBUS' }, null)).toBe('Bus error');
76
+ });
77
+
78
+ it('skips generic crash descriptions', () => {
79
+ const crashWithGeneric = { ...baseCrash, description: 'Application crash' };
80
+ expect(resolveCrashErrorMessage(crashWithGeneric, null)).toBe('SIGSEGV: Segmentation fault, status: 0');
81
+ });
82
+
83
+ it('recognizes all generic crash description variations (case-insensitive)', () => {
84
+ expect(resolveCrashErrorMessage({ ...baseCrash, description: 'CRASH' }, null)).toBe(
85
+ 'SIGSEGV: Segmentation fault, status: 0'
86
+ );
87
+ expect(resolveCrashErrorMessage({ ...baseCrash, description: 'Application Crash' }, null)).toBe(
88
+ 'SIGSEGV: Segmentation fault, status: 0'
89
+ );
90
+ expect(resolveCrashErrorMessage({ ...baseCrash, description: 'Signal Crash' }, null)).toBe(
91
+ 'SIGSEGV: Segmentation fault, status: 0'
92
+ );
93
+ });
94
+
95
+ it('uses simple signal description when no description is provided', () => {
96
+ expect(resolveCrashErrorMessage(baseCrash, null)).toBe('Segmentation fault');
97
+ });
98
+
99
+ it('uses exception reason alone with default "Exception" name', () => {
100
+ const parsed: ParsedIosCrashTrace = {
101
+ exceptionReason: 'Array index out of bounds',
102
+ frames: [],
103
+ };
104
+
105
+ expect(resolveCrashErrorMessage(baseCrash, parsed)).toBe('Exception: Array index out of bounds');
106
+ });
107
+
108
+ it('uses meaningful description even for unknown crash reason', () => {
109
+ const crashWithUnknownReason = { ...baseCrash, reason: 'CUSTOM_SIGNAL', description: 'Custom error' };
110
+ expect(resolveCrashErrorMessage(crashWithUnknownReason, null)).toBe('Custom error');
111
+ });
112
+
113
+ it('uses buildFallbackCrashMessage for unknown signal with no description', () => {
114
+ const crashWithUnknownReason = { reason: 'CUSTOM_SIGNAL', timestamp: 123, status: 5 };
115
+ expect(resolveCrashErrorMessage(crashWithUnknownReason, null)).toBe('CUSTOM_SIGNAL: Application crash, status: 5');
116
+ });
117
+
118
+ it('handles empty/whitespace strings correctly', () => {
119
+ const crashWithEmptyDescription = { ...baseCrash, description: ' ' };
120
+ expect(resolveCrashErrorMessage(crashWithEmptyDescription, null)).toBe('Segmentation fault');
121
+ });
122
+ });
123
+
124
+ describe('shouldSkipCrashReport', () => {
125
+ it('keeps crashes with trace', () => {
126
+ expect(shouldSkipCrashReport({ ...baseCrash, trace: '0 MyApp 0x100abc123' })).toBe(false);
127
+ });
128
+
129
+ it('keeps crashes with signal', () => {
130
+ expect(shouldSkipCrashReport({ ...baseCrash, signal: 'SIGSEGV (11)' })).toBe(false);
131
+ });
132
+
133
+ it('keeps crashes with meaningful description', () => {
134
+ expect(shouldSkipCrashReport({ ...baseCrash, description: 'Fatal error in module' })).toBe(false);
135
+ });
136
+
137
+ it('skips crashes with no useful information', () => {
138
+ expect(shouldSkipCrashReport({ reason: 'UNKNOWN', timestamp: 123, status: 0 })).toBe(true);
139
+ });
140
+
141
+ it('skips crashes with only generic descriptions', () => {
142
+ expect(shouldSkipCrashReport({ ...baseCrash, description: 'crash' })).toBe(true);
143
+ });
144
+
145
+ it('handles empty/whitespace trace and signal correctly', () => {
146
+ expect(shouldSkipCrashReport({ ...baseCrash, trace: ' ', signal: ' ' })).toBe(true);
147
+ });
148
+
149
+ it('keeps crashes with signal crash description if trace or signal present', () => {
150
+ expect(shouldSkipCrashReport({ ...baseCrash, trace: 'stack', description: 'signal crash' })).toBe(false);
151
+ });
152
+ });
153
+
154
+ describe('buildFallbackCrashMessage', () => {
155
+ it('builds fallback with known signal', () => {
156
+ expect(buildFallbackCrashMessage({ reason: 'SIGSEGV', timestamp: 123, status: 11 })).toBe(
157
+ 'SIGSEGV: Segmentation fault, status: 11'
158
+ );
159
+ });
160
+
161
+ it('uses description for unknown signals', () => {
162
+ expect(
163
+ buildFallbackCrashMessage({ reason: 'CUSTOM', timestamp: 123, status: 0, description: 'Custom crash' })
164
+ ).toBe('CUSTOM: Custom crash, status: 0');
165
+ });
166
+
167
+ it('handles missing reason', () => {
168
+ expect(buildFallbackCrashMessage({ timestamp: 123, status: 0 })).toBe('UNKNOWN: Application crash, status: 0');
169
+ });
170
+ });
@@ -0,0 +1,111 @@
1
+ import type { CrashReport } from '../types';
2
+
3
+ import type { ParsedIosCrashTrace } from './parseIosCrashTrace';
4
+
5
+ /**
6
+ * Human-readable exception value for iOS PLCrashReporter crash reports.
7
+ * Priority: exception reason → signal description → signal name → fallback.
8
+ */
9
+ export function resolveCrashErrorMessage(crash: CrashReport, parsedTrace: ParsedIosCrashTrace | null): string {
10
+ // iOS uncaught exceptions (NSException) have name and reason
11
+ if (parsedTrace?.exceptionName || parsedTrace?.exceptionReason) {
12
+ const name = parsedTrace.exceptionName || 'Exception';
13
+ const reason = parsedTrace.exceptionReason?.trim();
14
+ if (reason) {
15
+ return `${name}: ${reason}`;
16
+ }
17
+ return name;
18
+ }
19
+
20
+ // Use signal description if available from parsed trace
21
+ if (parsedTrace?.signalDescription?.trim()) {
22
+ return parsedTrace.signalDescription.trim();
23
+ }
24
+
25
+ // Use signal name from parsed trace
26
+ if (parsedTrace?.signalName?.trim()) {
27
+ const signalName = parsedTrace.signalName.trim();
28
+ const description = SIGNAL_DESCRIPTIONS[signalName];
29
+ return description || signalName;
30
+ }
31
+
32
+ // Check crash description - use if meaningful
33
+ const description = crash.description?.trim();
34
+ if (description && !isGenericCrashDescription(description)) {
35
+ return description;
36
+ }
37
+
38
+ // Check if we have a known signal reason
39
+ const reason = crash.reason?.trim();
40
+ if (reason && SIGNAL_DESCRIPTIONS[reason]) {
41
+ // If description is explicitly generic, use full format
42
+ if (description && isGenericCrashDescription(description)) {
43
+ return buildFallbackCrashMessage(crash);
44
+ }
45
+ // No description - use simple signal description
46
+ return SIGNAL_DESCRIPTIONS[reason];
47
+ }
48
+
49
+ // Final fallback: format with signal name, description, and status
50
+ return buildFallbackCrashMessage(crash);
51
+ }
52
+
53
+ /**
54
+ * Skip crash reports that have no useful information.
55
+ * iOS crashes should always have either a trace or a signal.
56
+ */
57
+ export function shouldSkipCrashReport(crash: CrashReport): boolean {
58
+ if (crash.trace?.trim()) {
59
+ return false;
60
+ }
61
+
62
+ if (crash.signal?.trim()) {
63
+ return false;
64
+ }
65
+
66
+ const description = crash.description?.trim();
67
+ if (description && !isGenericCrashDescription(description)) {
68
+ return false;
69
+ }
70
+
71
+ return true;
72
+ }
73
+
74
+ /** BSD signal names to human-readable descriptions */
75
+ const SIGNAL_DESCRIPTIONS: Record<string, string> = {
76
+ SIGHUP: 'Hangup',
77
+ SIGINT: 'Interrupt',
78
+ SIGQUIT: 'Quit',
79
+ SIGILL: 'Illegal instruction',
80
+ SIGTRAP: 'Trace/BPT trap',
81
+ SIGABRT: 'Abort trap',
82
+ SIGEMT: 'EMT trap',
83
+ SIGFPE: 'Floating point exception',
84
+ SIGKILL: 'Killed',
85
+ SIGBUS: 'Bus error',
86
+ SIGSEGV: 'Segmentation fault',
87
+ SIGSYS: 'Bad system call',
88
+ SIGPIPE: 'Broken pipe',
89
+ SIGALRM: 'Alarm clock',
90
+ SIGTERM: 'Terminated',
91
+ };
92
+
93
+ function isGenericCrashDescription(description: string): boolean {
94
+ const normalized = description.trim().toLowerCase();
95
+ return normalized === 'crash' || normalized === 'application crash' || normalized === 'signal crash';
96
+ }
97
+
98
+ /** Generic fallback when the trace carries no parseable signal or description. */
99
+ export function buildFallbackCrashMessage(crash: CrashReport): string {
100
+ const reason = crash.reason || 'UNKNOWN';
101
+ const status = crash.status ?? 0;
102
+
103
+ let description: string;
104
+ if (SIGNAL_DESCRIPTIONS[reason]) {
105
+ description = SIGNAL_DESCRIPTIONS[reason];
106
+ } else {
107
+ description = crash.description || 'Application crash';
108
+ }
109
+
110
+ return `${reason}: ${description}, status: ${status}`;
111
+ }
@@ -0,0 +1,4 @@
1
+ export { IosCrashReportingInstrumentation } from './IosCrashReportingInstrumentation';
2
+ export { parseIosCrashTrace } from './parseIosCrashTrace';
3
+ export type { ParsedIosCrashTrace, IosCrashStackFrame } from './parseIosCrashTrace';
4
+ export { resolveCrashErrorMessage, shouldSkipCrashReport } from './crashErrorMessage';
@@ -0,0 +1,86 @@
1
+ import { parseIosCrashTrace } from './parseIosCrashTrace';
2
+
3
+ describe('parseIosCrashTrace', () => {
4
+ it('parses iOS PLCrashReporter stack trace format', () => {
5
+ const trace = [
6
+ '0 libsystem_kernel.dylib 0x00000001a2b3c000',
7
+ '1 MyApp 0x0000000100abc123',
8
+ '2 UIKitCore 0x00000001b4567890',
9
+ '3 Foundation 0x00000001a8901234',
10
+ ].join('\n');
11
+
12
+ const parsed = parseIosCrashTrace(trace);
13
+
14
+ expect(parsed).toEqual({
15
+ frames: [
16
+ {
17
+ module: 'libsystem_kernel.dylib',
18
+ function: '0x00000001a2b3c000',
19
+ filename: 'libsystem_kernel.dylib',
20
+ lineno: 0,
21
+ instructionPointer: '0x00000001a2b3c000',
22
+ },
23
+ {
24
+ module: 'MyApp',
25
+ function: '0x0000000100abc123',
26
+ filename: 'MyApp',
27
+ lineno: 1,
28
+ instructionPointer: '0x0000000100abc123',
29
+ },
30
+ {
31
+ module: 'UIKitCore',
32
+ function: '0x00000001b4567890',
33
+ filename: 'UIKitCore',
34
+ lineno: 2,
35
+ instructionPointer: '0x00000001b4567890',
36
+ },
37
+ {
38
+ module: 'Foundation',
39
+ function: '0x00000001a8901234',
40
+ filename: 'Foundation',
41
+ lineno: 3,
42
+ instructionPointer: '0x00000001a8901234',
43
+ },
44
+ ],
45
+ });
46
+ });
47
+
48
+ it('returns null for empty input', () => {
49
+ expect(parseIosCrashTrace('')).toBeNull();
50
+ expect(parseIosCrashTrace(' ')).toBeNull();
51
+ });
52
+
53
+ it('handles stack traces with whitespace variations', () => {
54
+ const trace = [' 0 libsystem_kernel.dylib 0x00000001a2b3c000 ', '1 MyApp 0x0000000100abc123'].join('\n');
55
+
56
+ const parsed = parseIosCrashTrace(trace);
57
+
58
+ expect(parsed?.frames).toHaveLength(2);
59
+ expect(parsed?.frames[0]).toMatchObject({
60
+ module: 'libsystem_kernel.dylib',
61
+ instructionPointer: '0x00000001a2b3c000',
62
+ lineno: 0,
63
+ });
64
+ });
65
+
66
+ it('returns null when no valid frames are found', () => {
67
+ const trace = ['Not a valid frame', 'Another invalid line', 'Signal: SIGSEGV'].join('\n');
68
+
69
+ expect(parseIosCrashTrace(trace)).toBeNull();
70
+ });
71
+
72
+ it('handles mixed valid and invalid lines', () => {
73
+ const trace = [
74
+ 'Exception: NSInternalInconsistencyException',
75
+ '0 MyApp 0x0000000100abc123',
76
+ 'Some metadata line',
77
+ '1 UIKitCore 0x00000001b4567890',
78
+ ].join('\n');
79
+
80
+ const parsed = parseIosCrashTrace(trace);
81
+
82
+ expect(parsed?.frames).toHaveLength(2);
83
+ expect(parsed?.frames[0].module).toBe('MyApp');
84
+ expect(parsed?.frames[1].module).toBe('UIKitCore');
85
+ });
86
+ });
@@ -0,0 +1,83 @@
1
+ import type { ExceptionStackFrame } from '@grafana/faro-core';
2
+
3
+ /**
4
+ * iOS PLCrashReporter stack frames include library name and instruction pointers.
5
+ * Format: "0 libsystem_kernel.dylib 0x00000001a2b3c000"
6
+ */
7
+ export interface IosCrashStackFrame extends ExceptionStackFrame {
8
+ /** Library/module name (e.g., "MyApp", "UIKitCore") */
9
+ module: string;
10
+ /** Instruction pointer address (hex string) */
11
+ instructionPointer?: string;
12
+ /** Library base address (hex string) */
13
+ libraryBaseAddress?: string;
14
+ }
15
+
16
+ export interface ParseIosCrashTraceOptions {
17
+ releaseBundleFilename?: string;
18
+ }
19
+
20
+ export interface ParsedIosCrashTrace {
21
+ /** Signal name (e.g., "SIGSEGV", "SIGABRT") */
22
+ signalName?: string;
23
+ /** Human-readable signal description */
24
+ signalDescription?: string;
25
+ /** Exception name for uncaught exceptions (e.g., "NSInternalInconsistencyException") */
26
+ exceptionName?: string;
27
+ /** Exception reason/message for uncaught exceptions */
28
+ exceptionReason?: string;
29
+ /** iOS native frames (library + instruction pointer for symbolication) */
30
+ frames: IosCrashStackFrame[];
31
+ }
32
+
33
+ /**
34
+ * iOS PLCrashReporter stack trace line format:
35
+ * "0 libsystem_kernel.dylib 0x00000001a2b3c000"
36
+ * frame_number library_name instruction_pointer
37
+ */
38
+ const IOS_STACK_FRAME_LINE = /^\s*(\d+)\s+(.+?)\s+(0x[0-9a-fA-F]+)\s*$/;
39
+
40
+ /**
41
+ * Parse a raw iOS crash trace from PLCrashReporter into structured stack frames.
42
+ *
43
+ * iOS crashes from PLCrashReporter come with formatted stack traces where each line
44
+ * contains: frame number, library name, and instruction pointer hex address.
45
+ *
46
+ * The collector will use the library name and instruction pointer for symbolication
47
+ * with dSYM files or Breakpad symbols.
48
+ */
49
+ export function parseIosCrashTrace(trace: string, _options?: ParseIosCrashTraceOptions): ParsedIosCrashTrace | null {
50
+ const trimmed = trace.trim();
51
+ if (!trimmed) {
52
+ return null;
53
+ }
54
+
55
+ const lines = trimmed.split('\n');
56
+ const frames: IosCrashStackFrame[] = [];
57
+
58
+ for (const line of lines) {
59
+ const frameMatch = line.match(IOS_STACK_FRAME_LINE);
60
+
61
+ if (frameMatch) {
62
+ const [, frameNumber, libraryName, instructionPointer] = frameMatch;
63
+
64
+ if (!libraryName || !instructionPointer || !frameNumber) {
65
+ continue;
66
+ }
67
+
68
+ frames.push({
69
+ module: libraryName.trim(),
70
+ function: instructionPointer,
71
+ filename: libraryName.trim(),
72
+ lineno: parseInt(frameNumber, 10),
73
+ instructionPointer: instructionPointer,
74
+ });
75
+ }
76
+ }
77
+
78
+ if (frames.length === 0) {
79
+ return null;
80
+ }
81
+
82
+ return { frames };
83
+ }
@@ -1,70 +1,31 @@
1
1
  /**
2
- * Configuration options for Crash Reporting instrumentation.
3
- *
4
- * **Platform Support**:
5
- * - **Android**: Uses ApplicationExitInfo API (Android 11+ / API 30+)
6
- * Captures: CRASH, CRASH_NATIVE, ANR, LOW_MEMORY, EXCESSIVE_RESOURCE_USAGE
7
- * - **iOS**: Uses PLCrashReporter (automatically included via podspec)
8
- * Captures: Signal crashes (SIGSEGV, SIGABRT, etc.) and Mach exceptions
9
- */
10
- export interface CrashReportingOptions {
11
- /**
12
- * Whether to enable crash reporting.
13
- * Default: true
14
- */
15
- enabled?: boolean;
16
- }
17
-
18
- /**
19
- * Crash report data from native module.
20
- *
21
- * Field names match Faro Flutter SDK for consistency across mobile platforms.
22
- * Both Android (ApplicationExitInfo) and iOS (PLCrashReporter) produce JSON
23
- * matching this interface.
2
+ * Crash report data structure returned from native module.
24
3
  */
25
4
  export interface CrashReport {
26
- /**
27
- * Reason for the crash (e.g., "CRASH", "CRASH_NATIVE", "ANR", "LOW_MEMORY")
28
- */
29
- reason: string;
30
-
31
- /**
32
- * Timestamp when the crash occurred (milliseconds since epoch)
33
- */
34
- timestamp: number;
35
-
36
- /**
37
- * Exit status code
38
- */
39
- status?: number;
40
-
41
- /**
42
- * Description of the crash
43
- */
5
+ trace?: string;
6
+ signal?: string;
7
+ timestamp?: number;
44
8
  description?: string;
45
-
46
- /**
47
- * Process importance level
48
- */
49
- importance?: number;
50
-
51
- /**
52
- * Process ID
53
- */
54
- pid?: number;
55
-
56
- /**
57
- * Process name
58
- */
59
9
  processName?: string;
10
+ pid?: number;
11
+ importance?: number;
12
+ // iOS-specific fields
13
+ reason?: string; // Signal name (e.g., 'SIGSEGV')
14
+ status?: number; // Exit status code
15
+ }
60
16
 
17
+ /**
18
+ * Configuration options for crash reporting instrumentation.
19
+ */
20
+ export interface CrashReportingOptions {
61
21
  /**
62
- * Stack trace (if available)
22
+ * Enable or disable crash reporting. Defaults to true.
63
23
  */
64
- trace?: string;
24
+ enabled?: boolean;
65
25
 
66
26
  /**
67
- * Signal name (for iOS crashes)
27
+ * Optional release bundle filename hint for R8 retracing.
28
+ * e.g., "index.android.bundle"
68
29
  */
69
- signal?: string;
30
+ releaseBundleFilename?: string;
70
31
  }
@@ -136,7 +136,6 @@ export class ErrorsInstrumentation extends BaseInstrumentation {
136
136
  error,
137
137
  {
138
138
  mechanism: ErrorMechanism.UNCAUGHT,
139
- isFatal: String(isFatal ?? false),
140
139
  },
141
140
  { releaseBundleFilename: this.options.releaseBundleFilename }
142
141
  );
@@ -145,6 +144,7 @@ export class ErrorsInstrumentation extends BaseInstrumentation {
145
144
  this.api.pushError(enhancedError, {
146
145
  type: enhancedError.name || 'Error',
147
146
  context,
147
+ fatal: isFatal ?? false,
148
148
  stackFrames,
149
149
  });
150
150
 
@@ -209,6 +209,7 @@ export class ErrorsInstrumentation extends BaseInstrumentation {
209
209
  this.api.pushError(enhancedError, {
210
210
  type: errorType,
211
211
  context,
212
+ fatal: false,
212
213
  stackFrames,
213
214
  });
214
215