@grafana/faro-react-native 1.2.1 → 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 (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 +3 -3
  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
@@ -7,11 +7,13 @@ buildscript {
7
7
  dependencies {
8
8
  classpath 'com.android.tools.build:gradle:7.4.2'
9
9
  classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0'
10
+ classpath 'com.google.protobuf:protobuf-gradle-plugin:0.9.4'
10
11
  }
11
12
  }
12
13
 
13
14
  apply plugin: 'com.android.library'
14
15
  apply plugin: 'kotlin-android'
16
+ apply plugin: 'com.google.protobuf'
15
17
 
16
18
  android {
17
19
  namespace 'com.grafana.faro.reactnative'
@@ -20,17 +22,40 @@ android {
20
22
  defaultConfig {
21
23
  minSdkVersion 21
22
24
  targetSdkVersion 33
25
+ consumerProguardFiles 'consumer-proguard-rules.pro'
23
26
  }
24
27
 
25
28
  sourceSets {
26
29
  main {
27
30
  java.srcDirs = ['src/main/java']
31
+ proto {
32
+ srcDir 'src/main/proto'
33
+ }
28
34
  }
29
35
  }
30
36
 
31
37
  lintOptions {
32
38
  abortOnError false
33
39
  }
40
+
41
+ testOptions {
42
+ unitTests.returnDefaultValues = true
43
+ }
44
+ }
45
+
46
+ protobuf {
47
+ protoc {
48
+ artifact = 'com.google.protobuf:protoc:3.25.1'
49
+ }
50
+ generateProtoTasks {
51
+ all().each { task ->
52
+ task.builtins {
53
+ java {
54
+ option 'lite'
55
+ }
56
+ }
57
+ }
58
+ }
34
59
  }
35
60
 
36
61
  repositories {
@@ -42,6 +67,11 @@ dependencies {
42
67
  //noinspection GradleDynamicVersion
43
68
  implementation 'com.facebook.react:react-native:+'
44
69
  implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.0'
70
+ implementation 'com.google.protobuf:protobuf-javalite:3.25.1'
71
+
72
+ testImplementation 'junit:junit:4.13.2'
73
+ testImplementation 'org.robolectric:robolectric:4.11.1'
74
+ testImplementation 'androidx.test:core:1.5.0'
45
75
  }
46
76
 
47
77
  /* ---------------------------------------------------------------------------
@@ -53,8 +83,9 @@ dependencies {
53
83
  *
54
84
  * 1. npm install @grafana/faro-react-native @grafana/faro-metro-plugin
55
85
  * 2. Wrap metro.config.js with `withFaroConfig(...)`
56
- * 3. Export FARO_BUNDLE_ID + FARO_SOURCEMAP_* before running
57
- * `yarn android --mode=release`.
86
+ * 3. Export FARO_SOURCEMAP_* before running `yarn android --mode=release`.
87
+ * Bundle id is read from `app/build/faro/bundle-id-release.txt` when the
88
+ * `com.grafana.faro` Gradle plugin is applied (same id Metro uses).
58
89
  *
59
90
  * On any release-style command (assembleRelease, bundleRelease, installRelease,
60
91
  * `yarn android --mode=release`), release entry tasks depend on
@@ -62,21 +93,20 @@ dependencies {
62
93
  * the JS bundle task (`bundleReleaseJsAndAssets` or `createBundleReleaseJsAndAssets`)
63
94
  * so upload runs only after a successful bundle — never via a finalizer (which
64
95
  * can run even when bundling fails and could upload a stale map). The upload
65
- * task runs `faro-cli metro upload` via a small Node shim. Gradle reads every
66
- * required setting from env vars and passes them as **explicit CLI arguments** so the CLI never has to fall
67
- * back to its own env-var resolution from this entry point — making the
68
- * contract between Gradle and the CLI fully visible in the command line that
69
- * you'll see in `--info` build output.
96
+ * task runs `faro-cli metro upload` via a small Node shim. Bundle id comes from
97
+ * the Gradle plugin file; upload credentials from env or sourcemaps.config.json.
98
+ * Credentials are forwarded as **process environment variables** (not CLI flags)
99
+ * so secrets do not appear in `ps` or Gradle `--info` command lines.
100
+ *
101
+ * Required at build time:
102
+ * Bundle id — `app/build/faro/bundle-id-release.txt` from `com.grafana.faro`
103
+ * (`faroWriteBundleIdRelease`). Same id Metro uses. Forwarded as --bundle-id.
70
104
  *
71
- * Required env at build time (Gradle forwards each as a CLI flag):
72
- * FARO_BUNDLE_ID — must match the id baked into the JS bundle
73
- * preamble by @grafana/faro-metro-plugin so
74
- * meta.app.bundleId on the device matches
75
- * the uploaded map. Forwarded as --bundle-id.
76
- * FARO_SOURCEMAP_ENDPOINT — Forwarded as --endpoint.
77
- * FARO_SOURCEMAP_APP_ID — Forwarded as --app-id.
78
- * FARO_SOURCEMAP_STACK_ID — Forwarded as --stack-id.
79
- * FARO_SOURCEMAP_API_KEY — Forwarded as --api-key.
105
+ * Required env (Gradle forwards to the Node child process):
106
+ * FARO_SOURCEMAP_ENDPOINT
107
+ * FARO_SOURCEMAP_APP_ID
108
+ * FARO_SOURCEMAP_STACK_ID
109
+ * FARO_SOURCEMAP_API_KEY
80
110
  *
81
111
  * Optional env at build time:
82
112
  * FARO_SKIP_SOURCEMAP_UPLOAD=1 — skip the upload (e.g. offline iteration).
@@ -109,6 +139,45 @@ gradle.projectsEvaluated {
109
139
  def jsRoot = rootProject.projectDir.parentFile
110
140
  def cliPath = new File(jsRoot, "node_modules/@grafana/faro-metro-plugin/bin/faro-upload-source-map.js")
111
141
  def composedMap = new File(appProject.buildDir, "generated/sourcemaps/react/release/index.android.bundle.map")
142
+ def bundleIdFile = new File(appProject.buildDir, "faro/bundle-id-release.txt")
143
+
144
+ // Security: sourcemaps.config.json is a fallback for local dev only.
145
+ // In CI/production, prefer FARO_SOURCEMAP_* env vars to avoid committing credentials.
146
+ // Add sourcemaps.config.json to .gitignore if it contains apiKey.
147
+ def readSourcemapsConfigValue = { String key ->
148
+ def configFile = new File(jsRoot, "sourcemaps.config.json")
149
+ if (!configFile.exists()) {
150
+ return ""
151
+ }
152
+ try {
153
+ def parsed = new groovy.json.JsonSlurper().parse(configFile)
154
+ def val = parsed[key]
155
+ return val == null ? "" : val.toString().trim()
156
+ } catch (Exception e) {
157
+ logger.error("[Faro] Failed to parse ${configFile}: ${e.message}")
158
+ return ""
159
+ }
160
+ }
161
+
162
+ def envOrSourcemapsConfig = { String envName, String configKey ->
163
+ def fromEnv = (System.getenv(envName) ?: "").trim()
164
+ if (!fromEnv.isEmpty()) {
165
+ return fromEnv
166
+ }
167
+ return readSourcemapsConfigValue(configKey)
168
+ }
169
+
170
+ def resolveFaroBundleId = {
171
+ if (!bundleIdFile.exists()) {
172
+ return ""
173
+ }
174
+ def fromFile = bundleIdFile.getText("UTF-8").trim()
175
+ return fromFile.isEmpty() ? "" : fromFile
176
+ }
177
+
178
+ def isValidAndroidBundleId = { String id ->
179
+ id ==~ /^[^@]+@\d+@[^@]+$/
180
+ }
112
181
 
113
182
  def uploadTask = appProject.tasks.register("faroUploadComposedSourceMapAndroidRelease", Exec) {
114
183
  group = "react"
@@ -117,17 +186,16 @@ gradle.projectsEvaluated {
117
186
  workingDir = jsRoot
118
187
 
119
188
  def faroSkip = (System.getenv("FARO_SKIP_SOURCEMAP_UPLOAD") ?: "").trim().toLowerCase()
120
- def faroBundleId = (System.getenv("FARO_BUNDLE_ID") ?: "").trim()
121
- def faroEndpoint = (System.getenv("FARO_SOURCEMAP_ENDPOINT") ?: "").trim()
122
- def faroAppId = (System.getenv("FARO_SOURCEMAP_APP_ID") ?: "").trim()
123
- def faroStackId = (System.getenv("FARO_SOURCEMAP_STACK_ID") ?: "").trim()
124
- def faroApiKey = (System.getenv("FARO_SOURCEMAP_API_KEY") ?: "").trim()
189
+ def faroEndpoint = envOrSourcemapsConfig("FARO_SOURCEMAP_ENDPOINT", "endpoint")
190
+ def faroAppId = envOrSourcemapsConfig("FARO_SOURCEMAP_APP_ID", "appId")
191
+ def faroStackId = envOrSourcemapsConfig("FARO_SOURCEMAP_STACK_ID", "stackId")
192
+ def faroApiKey = envOrSourcemapsConfig("FARO_SOURCEMAP_API_KEY", "apiKey")
125
193
 
126
194
  // Capture the inputs we use for caching / up-to-date checks. We
127
195
  // deliberately *don't* track FARO_SOURCEMAP_API_KEY in inputs to
128
196
  // avoid Gradle leaking it into a cache-key string.
129
197
  inputs.file(composedMap).withPropertyName("composedMap").optional()
130
- inputs.property("faroBundleId", faroBundleId)
198
+ inputs.file(bundleIdFile).withPropertyName("bundleIdFile").optional()
131
199
  inputs.property("faroEndpoint", faroEndpoint)
132
200
  inputs.property("faroAppId", faroAppId)
133
201
  inputs.property("faroStackId", faroStackId)
@@ -139,40 +207,54 @@ gradle.projectsEvaluated {
139
207
  return false
140
208
  }
141
209
  if (!cliPath.exists()) {
142
- logger.warn("[Faro] Skipping composed source map upload — CLI shim not found at ${cliPath}. Install @grafana/faro-metro-plugin alongside @grafana/faro-react-native.")
210
+ logger.error("[Faro] Skipping composed source map upload — CLI shim not found at ${cliPath}. Install @grafana/faro-metro-plugin alongside @grafana/faro-react-native.")
143
211
  return false
144
212
  }
145
213
  if (!composedMap.exists()) {
146
- logger.warn("[Faro] Skipping composed source map upload — ${composedMap} does not exist. Did Hermes precompile run?")
214
+ logger.error("[Faro] Skipping composed source map upload — ${composedMap} does not exist. Did Hermes precompile run?")
147
215
  return false
148
216
  }
217
+ def resolvedBundleId = resolveFaroBundleId()
149
218
  def missing = []
150
- if (faroBundleId.isEmpty()) missing << "FARO_BUNDLE_ID"
219
+ if (resolvedBundleId.isEmpty()) {
220
+ missing << "bundle id file ${bundleIdFile} (apply com.grafana.faro and run faroWriteBundleIdRelease)"
221
+ } else if (!isValidAndroidBundleId(resolvedBundleId)) {
222
+ logger.error("[Faro] Skipping composed source map upload — invalid bundle id \"${resolvedBundleId}\" in ${bundleIdFile} (expected applicationId@versionCode@versionName).")
223
+ return false
224
+ }
151
225
  if (faroEndpoint.isEmpty()) missing << "FARO_SOURCEMAP_ENDPOINT"
152
226
  if (faroAppId.isEmpty()) missing << "FARO_SOURCEMAP_APP_ID"
153
227
  if (faroStackId.isEmpty()) missing << "FARO_SOURCEMAP_STACK_ID"
154
228
  if (faroApiKey.isEmpty()) missing << "FARO_SOURCEMAP_API_KEY"
155
229
  if (!missing.isEmpty()) {
156
- logger.warn("[Faro] Skipping composed source map upload — missing required env vars: ${missing.join(", ")}. Export them before running `yarn android --mode=release`. FARO_BUNDLE_ID must match the id baked into the bundle preamble by @grafana/faro-metro-plugin.")
230
+ logger.error("[Faro] Skipping composed source map upload — missing: ${missing.join(", ")}. Set FARO_SOURCEMAP_* or fill sourcemaps.config.json (endpoint, appId, stackId, apiKey). Bundle id is read from ${bundleIdFile}.")
157
231
  return false
158
232
  }
233
+ ext.resolvedBundleId = resolvedBundleId
159
234
  return true
160
235
  }
161
236
 
162
- commandLine(
163
- "node", cliPath.absolutePath,
164
- "--verbose",
165
- "--map", composedMap.absolutePath,
166
- "--endpoint", faroEndpoint,
167
- "--app-id", faroAppId,
168
- "--stack-id", faroStackId,
169
- "--api-key", faroApiKey,
170
- "--bundle-id", faroBundleId,
171
- )
237
+ doFirst {
238
+ def bundleId = ext.resolvedBundleId ?: resolveFaroBundleId()
239
+ environment 'FARO_SOURCEMAP_ENDPOINT', faroEndpoint
240
+ environment 'FARO_SOURCEMAP_APP_ID', faroAppId
241
+ environment 'FARO_SOURCEMAP_STACK_ID', faroStackId
242
+ environment 'FARO_SOURCEMAP_API_KEY', faroApiKey
243
+ commandLine(
244
+ "node", cliPath.absolutePath,
245
+ "--verbose",
246
+ "--map", composedMap.absolutePath,
247
+ "--bundle-id", bundleId,
248
+ )
249
+ }
172
250
  }
173
251
 
174
252
  def bundleTaskNames = ["bundleReleaseJsAndAssets", "createBundleReleaseJsAndAssets"]
175
253
  uploadTask.configure { task ->
254
+ def writeBundleIdTask = appProject.tasks.findByName("faroWriteBundleIdRelease")
255
+ if (writeBundleIdTask != null) {
256
+ task.dependsOn(writeBundleIdTask)
257
+ }
176
258
  bundleTaskNames.each { name ->
177
259
  def bundleTask = appProject.tasks.findByName(name)
178
260
  if (bundleTask != null) {
@@ -0,0 +1,2 @@
1
+ # Keep generated tombstone protobuf classes when the host app minifies release builds.
2
+ -keep class com.grafana.faro.reactnative.tombstone.** { *; }
@@ -1,5 +1,6 @@
1
1
  package com.grafana.faro.reactnative
2
2
 
3
+ import android.content.Context
3
4
  import android.os.Handler
4
5
  import android.os.Looper
5
6
  import android.util.Log
@@ -20,7 +21,9 @@ class ANRTracker : Thread("ANRTracker") {
20
21
  private const val TAG = "ANRTracker"
21
22
 
22
23
  /**
23
- * Time interval for checking ANR, in milliseconds (default 5 seconds)
24
+ * Watchdog check interval in milliseconds (default 5 seconds).
25
+ * Updated from JS via [FaroReactNativeModule.startANRTracking].
26
+ * Exported in ANR payloads as `duration` — the detection threshold, not measured block time.
24
27
  */
25
28
  var timeout: Long = 5000L
26
29
 
@@ -49,13 +52,36 @@ class ANRTracker : Thread("ANRTracker") {
49
52
  }
50
53
 
51
54
  /**
52
- * Clear the ANR events list
55
+ * Application context used to persist ANRs before JS delivery.
53
56
  */
54
- fun resetANR() {
57
+ @Volatile
58
+ var applicationContext: Context? = null
59
+
60
+ /**
61
+ * Remove in-memory ANRs that were acknowledged on disk.
62
+ */
63
+ fun acknowledgeInMemory(timestamps: Collection<Long>) {
64
+ if (timestamps.isEmpty()) {
65
+ return
66
+ }
67
+ val timestampSet = timestamps.toSet()
55
68
  synchronized(anrListLock) {
56
- anrList.clear()
69
+ anrList.removeAll { json ->
70
+ try {
71
+ JSONObject(json).optLong("timestamp", 0L) in timestampSet
72
+ } catch (_: Exception) {
73
+ false
74
+ }
75
+ }
57
76
  }
58
77
  }
78
+
79
+ /**
80
+ * Optional callback invoked on the ANR tracker thread when an ANR is recorded.
81
+ * Used to push telemetry immediately instead of waiting for the JS poll interval.
82
+ */
83
+ @Volatile
84
+ var onAnrDetected: ((String) -> Unit)? = null
59
85
  }
60
86
 
61
87
  private val mainHandler = Handler(Looper.getMainLooper())
@@ -63,6 +89,9 @@ class ANRTracker : Thread("ANRTracker") {
63
89
  private val isRunning = AtomicBoolean(true)
64
90
  private val taskExecuted = AtomicBoolean(false)
65
91
 
92
+ /** True while the main thread is blocked and we already recorded one ANR for this period. */
93
+ private val anrInProgress = AtomicBoolean(false)
94
+
66
95
  private val checkTask = Runnable {
67
96
  // This task runs on the main thread
68
97
  taskExecuted.set(true)
@@ -91,6 +120,11 @@ class ANRTracker : Thread("ANRTracker") {
91
120
  break
92
121
  }
93
122
 
123
+ if (taskExecuted.get()) {
124
+ // Main thread recovered — allow a future blocked period to be reported again.
125
+ anrInProgress.set(false)
126
+ }
127
+
94
128
  // If the task hasn't executed after the check interval, start monitoring for ANR
95
129
  if (!taskExecuted.get()) {
96
130
  Log.d(TAG, "Task not executed after initial check")
@@ -114,8 +148,14 @@ class ANRTracker : Thread("ANRTracker") {
114
148
  // Check again if the task has executed
115
149
  if (!taskExecuted.get()) {
116
150
  Log.d(TAG, "Task is still not executed after ${timeout}ms")
117
- // The main thread is blocked - this is an ANR
118
- handleAnrDetected()
151
+ // The main thread is blocked - record once per blocked period.
152
+ if (!anrInProgress.get()) {
153
+ anrInProgress.set(true)
154
+ handleAnrDetected()
155
+ }
156
+ } else if (anrInProgress.get()) {
157
+ // Main thread recovered during the extended wait — same cycle as line 123.
158
+ anrInProgress.set(false)
119
159
  }
120
160
  }
121
161
 
@@ -175,9 +215,12 @@ class ANRTracker : Thread("ANRTracker") {
175
215
  val stackTrace = mainThread.stackTrace
176
216
 
177
217
  // Build a readable stack trace
218
+ // Match Android Log.getStackTraceString / ApplicationExitInfo format so
219
+ // ingest R8 retrace and parseAndroidCrashTrace can consume the text.
178
220
  val stackTraceStr = StringBuilder()
179
221
  for (element in stackTrace) {
180
- stackTraceStr.append(element.className)
222
+ stackTraceStr.append(" at ")
223
+ .append(element.className)
181
224
  .append(".")
182
225
  .append(element.methodName)
183
226
  .append("(")
@@ -193,12 +236,21 @@ class ANRTracker : Thread("ANRTracker") {
193
236
  put("timestamp", System.currentTimeMillis())
194
237
  put("stacktrace", stackTraceStr.toString())
195
238
  put("duration", timeout)
239
+ put("source", "ANRTracker")
196
240
  }
197
241
 
198
- // Store the ANR information
242
+ val payload = anrInfo.toString()
243
+
244
+ // Persist first: survives process kill before JS/network can run.
245
+ applicationContext?.let { context ->
246
+ FaroAnrCache.savePendingAnr(context, payload)
247
+ }
248
+
199
249
  synchronized(anrListLock) {
200
- anrList.add(anrInfo.toString())
250
+ anrList.add(payload)
201
251
  }
252
+
253
+ onAnrDetected?.invoke(payload)
202
254
 
203
255
  Log.w(TAG, "ANR detected: $stackTraceStr")
204
256
  } catch (e: Exception) {
@@ -0,0 +1,70 @@
1
+ package com.grafana.faro.reactnative
2
+
3
+ /**
4
+ * Extracts the blocked main-thread stack from an ApplicationExitInfo ANR trace dump.
5
+ *
6
+ * Android emits a multi-thread text blob (see `ApplicationExitInfo.traceInputStream`).
7
+ * Sentry-style ANR reporting uses the `"main"` thread section, not a live
8
+ * [Thread.getStackTrace] snapshot from a watchdog thread.
9
+ */
10
+ internal object AnrThreadDumpParser {
11
+
12
+ private val threadHeader = Regex("^\"[^\"]+\".*prio=")
13
+ private val frameWithAt = Regex("^at\\s+.+")
14
+ private val frameWithoutAt = Regex("^[\\w$.]+\\.[\\w$<>]+\\(.+\\)\\s*$")
15
+
16
+ fun extractMainThreadStack(fullDump: String): String {
17
+ if (fullDump.isBlank()) {
18
+ return ""
19
+ }
20
+
21
+ val lines = fullDump.lines()
22
+ var inMain = false
23
+ val stack = StringBuilder()
24
+
25
+ for (line in lines) {
26
+ val trimmed = line.trim()
27
+ if (trimmed.isEmpty()) {
28
+ continue
29
+ }
30
+
31
+ if (!inMain) {
32
+ if (trimmed.startsWith("\"main\"") || trimmed == "main") {
33
+ inMain = true
34
+ }
35
+ continue
36
+ }
37
+
38
+ if (threadHeader.matches(trimmed) && !trimmed.startsWith("\"main\"")) {
39
+ break
40
+ }
41
+
42
+ if (frameWithAt.matches(trimmed)) {
43
+ stack.appendLine(if (line.startsWith(" ")) line else " $trimmed")
44
+ continue
45
+ }
46
+
47
+ if (frameWithoutAt.matches(trimmed)) {
48
+ stack.appendLine(" at $trimmed")
49
+ }
50
+ }
51
+
52
+ if (stack.isNotEmpty()) {
53
+ return stack.toString().trim()
54
+ }
55
+
56
+ // Some devices return only flat "at …" lines without a DALVIK THREADS header.
57
+ return extractFlatAtLines(fullDump)
58
+ }
59
+
60
+ private fun extractFlatAtLines(fullDump: String): String {
61
+ val stack = StringBuilder()
62
+ for (line in fullDump.lines()) {
63
+ val trimmed = line.trim()
64
+ if (frameWithAt.matches(trimmed)) {
65
+ stack.appendLine(if (line.startsWith(" ")) line else " $trimmed")
66
+ }
67
+ }
68
+ return stack.toString().trim()
69
+ }
70
+ }
@@ -0,0 +1,142 @@
1
+ package com.grafana.faro.reactnative
2
+
3
+ import android.app.ApplicationExitInfo
4
+ import android.os.Build
5
+ import android.util.Log
6
+ import java.io.ByteArrayOutputStream
7
+
8
+ /**
9
+ * Reads and normalizes ApplicationExitInfo trace streams into text tombstones.
10
+ */
11
+ internal object ApplicationExitTraceReader {
12
+
13
+ private const val TAG = "FaroCrashReporter"
14
+ private const val MAX_TRACE_BYTES = 512 * 1024
15
+ private const val MAX_TRACE_LINES = 200
16
+
17
+ data class ParsedExitTrace(
18
+ val text: String,
19
+ val signal: String = "",
20
+ val source: TraceSource = TraceSource.NONE,
21
+ )
22
+
23
+ enum class TraceSource {
24
+ NONE,
25
+ TEXT_STREAM,
26
+ PROTOBUF_TOMBSTONE,
27
+ CACHED_NATIVE,
28
+ }
29
+
30
+ fun read(exitInfo: ApplicationExitInfo): ParsedExitTrace {
31
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
32
+ return ParsedExitTrace("")
33
+ }
34
+
35
+ return try {
36
+ val traceInputStream = exitInfo.traceInputStream ?: return ParsedExitTrace("")
37
+ val bytes = readAllBytes(traceInputStream)
38
+ if (bytes.isEmpty()) {
39
+ return ParsedExitTrace("")
40
+ }
41
+
42
+ parseTraceBytes(exitInfo, bytes)
43
+ } catch (error: Exception) {
44
+ Log.w(TAG, "Failed to read ApplicationExitInfo trace: ${error.message}")
45
+ ParsedExitTrace("")
46
+ }
47
+ }
48
+
49
+ fun parseTraceBytes(exitInfo: ApplicationExitInfo, bytes: ByteArray): ParsedExitTrace {
50
+ val asText = decodeUtf8(bytes)
51
+ if (TombstoneBacktraceFormatter.looksLikeTextTombstone(asText)) {
52
+ Log.i(TAG, "[Faro crash native] Using text tombstone from ApplicationExitInfo (${asText.lineCount()} lines)")
53
+ return ParsedExitTrace(
54
+ text = trimTraceLines(asText),
55
+ source = TraceSource.TEXT_STREAM,
56
+ )
57
+ }
58
+
59
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
60
+ exitInfo.reason == ApplicationExitInfo.REASON_CRASH_NATIVE
61
+ ) {
62
+ return parseProtobufTombstone(bytes)
63
+ }
64
+
65
+ if (asText.isNotBlank()) {
66
+ return ParsedExitTrace(
67
+ text = trimTraceLines(asText),
68
+ source = TraceSource.TEXT_STREAM,
69
+ )
70
+ }
71
+
72
+ return ParsedExitTrace("")
73
+ }
74
+
75
+ private fun parseProtobufTombstone(bytes: ByteArray): ParsedExitTrace {
76
+ val parsed = TombstoneWireParser.parse(bytes)
77
+ if (parsed == null) {
78
+ Log.w(TAG, "[Faro crash native] Failed to parse tombstone protobuf (${bytes.size} bytes)")
79
+ return ParsedExitTrace("")
80
+ }
81
+
82
+ val formatted = TombstoneBacktraceFormatter.format(parsed)
83
+ if (formatted.text.isBlank()) {
84
+ Log.w(TAG, "[Faro crash native] Tombstone protobuf parsed but backtrace was empty")
85
+ return ParsedExitTrace("")
86
+ }
87
+
88
+ Log.i(
89
+ TAG,
90
+ "[Faro crash native] Decoded tombstone protobuf (${formatted.text.lineCount()} lines, signal=${formatted.signal})",
91
+ )
92
+ return ParsedExitTrace(
93
+ text = trimTraceLines(formatted.text),
94
+ signal = formatted.signal,
95
+ source = TraceSource.PROTOBUF_TOMBSTONE,
96
+ )
97
+ }
98
+
99
+ private fun readAllBytes(input: java.io.InputStream): ByteArray {
100
+ val buffer = ByteArrayOutputStream()
101
+ val chunk = ByteArray(8192)
102
+ var total = 0
103
+ while (true) {
104
+ val read = input.read(chunk)
105
+ if (read <= 0) {
106
+ break
107
+ }
108
+ total += read
109
+ if (total > MAX_TRACE_BYTES) {
110
+ Log.w(TAG, "ApplicationExitInfo trace exceeded ${MAX_TRACE_BYTES} bytes; truncating")
111
+ buffer.write(chunk, 0, read.coerceAtMost(MAX_TRACE_BYTES - (total - read)))
112
+ break
113
+ }
114
+ buffer.write(chunk, 0, read)
115
+ }
116
+ return buffer.toByteArray()
117
+ }
118
+
119
+ private fun decodeUtf8(bytes: ByteArray): String {
120
+ if (bytes.isEmpty()) {
121
+ return ""
122
+ }
123
+ if (bytes.any { it == 0.toByte() }) {
124
+ return ""
125
+ }
126
+ return bytes.toString(Charsets.UTF_8).trim()
127
+ }
128
+
129
+ private fun trimTraceLines(text: String): String {
130
+ return text.lineSequence()
131
+ .take(MAX_TRACE_LINES)
132
+ .joinToString("\n")
133
+ .trim()
134
+ }
135
+
136
+ private fun String.lineCount(): Int {
137
+ if (isEmpty()) {
138
+ return 0
139
+ }
140
+ return lines().size
141
+ }
142
+ }