@arms/rum-reactnative 0.1.2 → 0.1.3-beta.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 (172) hide show
  1. package/README.md +79 -1
  2. package/RumSdkReactnative.podspec +28 -0
  3. package/android/build.gradle +83 -0
  4. package/android/gradle.properties +6 -0
  5. package/android/src/main/AndroidManifest.xml +13 -0
  6. package/android/src/main/java/com/rumsdkreactnative/RumSdkReactnativeModule.kt +512 -0
  7. package/android/src/main/java/com/rumsdkreactnative/RumSdkReactnativePackage.kt +33 -0
  8. package/android/src/main/java/com/rumsdkreactnative/crash/CrashContextManager.kt +157 -0
  9. package/android/src/main/java/com/rumsdkreactnative/crash/CrashHandler.kt +444 -0
  10. package/android/src/main/java/com/rumsdkreactnative/crash/CrashInitProvider.kt +50 -0
  11. package/android/src/main/java/com/rumsdkreactnative/crash/NdkCrashHandler.kt +66 -0
  12. package/android/src/main/jni/CMakeLists.txt +21 -0
  13. package/android/src/main/jni/rum_backtrace.c +144 -0
  14. package/android/src/main/jni/rum_crash_handler.c +200 -0
  15. package/android/src/main/jni/rum_crash_writer.c +442 -0
  16. package/android/src/main/jni/rum_module_info.c +316 -0
  17. package/android/src/main/jni/rum_module_info.h +54 -0
  18. package/android/src/main/jni/rum_test_crash.cpp +55 -0
  19. package/app.plugin.js +11 -0
  20. package/docs/ReactNative SDK/351/205/215/347/275/256/345/217/202/350/200/203.md" +671 -0
  21. package/docs/SDK /347/211/210/346/234/254/350/257/264/346/230/216.md" +12 -0
  22. package/docs//346/216/245/345/205/245ReactNative/345/272/224/347/224/250.md +238 -0
  23. package/ios/PrivacyInfo.xcprivacy +93 -0
  24. package/ios/RumCrashReporter.h +38 -0
  25. package/ios/RumCrashReporter.mm +861 -0
  26. package/ios/RumSdkReactnative.h +5 -0
  27. package/ios/RumSdkReactnative.mm +291 -0
  28. package/ios/fishhook/fishhook.c +264 -0
  29. package/ios/fishhook/fishhook.h +76 -0
  30. package/lib/commonjs/NativeRumSdkReactnative.js +50 -0
  31. package/lib/commonjs/collector/action/index.js +108 -0
  32. package/lib/commonjs/collector/exception/crash.js +148 -0
  33. package/lib/commonjs/collector/exception/index.js +238 -0
  34. package/lib/commonjs/collector/index.js +42 -0
  35. package/lib/commonjs/collector/resource/api.js +286 -0
  36. package/lib/commonjs/collector/view/perf.js +79 -0
  37. package/lib/commonjs/collector/view/pv.js +88 -0
  38. package/lib/commonjs/expoConfigPlugin.js +37 -0
  39. package/lib/commonjs/index.js +29 -0
  40. package/lib/commonjs/package.json +1 -0
  41. package/lib/commonjs/plugin.js +47 -0
  42. package/lib/commonjs/processor/default-processor.js +27 -0
  43. package/lib/commonjs/processor/session-processor.js +47 -0
  44. package/lib/commonjs/reporter/index.js +148 -0
  45. package/lib/commonjs/reporter/monitor-reporter.js +43 -0
  46. package/lib/commonjs/shell.js +91 -0
  47. package/lib/commonjs/types/client.js +6 -0
  48. package/lib/commonjs/types/crash.js +37 -0
  49. package/lib/commonjs/types/index.js +2 -0
  50. package/lib/commonjs/utils/action.js +305 -0
  51. package/lib/commonjs/utils/api.js +83 -0
  52. package/lib/commonjs/utils/base.js +95 -0
  53. package/lib/commonjs/utils/crash.js +146 -0
  54. package/lib/commonjs/utils/platform.js +162 -0
  55. package/lib/commonjs/utils/session.js +158 -0
  56. package/lib/commonjs/utils/url.js +59 -0
  57. package/lib/commonjs/utils/view.js +93 -0
  58. package/lib/module/NativeRumSdkReactnative.js +47 -0
  59. package/lib/module/collector/action/index.js +45 -10
  60. package/lib/module/collector/exception/crash.js +143 -0
  61. package/lib/module/collector/exception/index.js +144 -46
  62. package/lib/module/collector/index.js +8 -0
  63. package/lib/module/collector/resource/api.js +6 -6
  64. package/lib/module/collector/view/pv.js +11 -0
  65. package/lib/module/expoConfigPlugin.js +33 -0
  66. package/lib/module/index.js +1 -0
  67. package/lib/module/processor/session-processor.js +22 -0
  68. package/lib/module/reporter/index.js +102 -16
  69. package/lib/module/reporter/monitor-reporter.js +38 -0
  70. package/lib/module/shell.js +11 -7
  71. package/lib/module/types/crash.js +33 -0
  72. package/lib/module/utils/action.js +299 -0
  73. package/lib/module/utils/crash.js +141 -0
  74. package/lib/module/utils/platform.js +124 -72
  75. package/lib/module/utils/session.js +23 -4
  76. package/lib/typescript/commonjs/package.json +1 -0
  77. package/lib/typescript/commonjs/src/NativeRumSdkReactnative.d.ts +49 -0
  78. package/lib/typescript/{src → commonjs/src}/collector/action/index.d.ts +6 -0
  79. package/lib/typescript/commonjs/src/collector/exception/crash.d.ts +28 -0
  80. package/lib/typescript/{src → commonjs/src}/collector/exception/index.d.ts +7 -0
  81. package/lib/typescript/commonjs/src/collector/index.d.ts +6 -0
  82. package/lib/typescript/commonjs/src/expoConfigPlugin.d.ts +28 -0
  83. package/lib/typescript/commonjs/src/index.d.ts +6 -0
  84. package/lib/typescript/{src → commonjs/src}/processor/session-processor.d.ts +2 -0
  85. package/lib/typescript/{src → commonjs/src}/reporter/index.d.ts +3 -1
  86. package/lib/typescript/commonjs/src/reporter/monitor-reporter.d.ts +8 -0
  87. package/lib/typescript/{src → commonjs/src}/shell.d.ts +11 -0
  88. package/lib/typescript/commonjs/src/types/crash.d.ts +95 -0
  89. package/lib/typescript/commonjs/src/utils/action.d.ts +63 -0
  90. package/lib/typescript/commonjs/src/utils/crash.d.ts +12 -0
  91. package/lib/typescript/commonjs/src/utils/platform.d.ts +22 -0
  92. package/lib/typescript/{src → commonjs/src}/utils/session.d.ts +4 -0
  93. package/lib/typescript/module/src/NativeRumSdkReactnative.d.ts +49 -0
  94. package/lib/typescript/module/src/collector/action/index.d.ts +21 -0
  95. package/lib/typescript/module/src/collector/exception/crash.d.ts +28 -0
  96. package/lib/typescript/module/src/collector/exception/index.d.ts +32 -0
  97. package/lib/typescript/module/src/collector/index.d.ts +6 -0
  98. package/lib/typescript/module/src/collector/resource/api.d.ts +21 -0
  99. package/lib/typescript/module/src/collector/view/perf.d.ts +1 -0
  100. package/lib/typescript/module/src/collector/view/pv.d.ts +16 -0
  101. package/lib/typescript/module/src/expoConfigPlugin.d.ts +28 -0
  102. package/lib/typescript/module/src/index.d.ts +6 -0
  103. package/lib/typescript/module/src/plugin.d.ts +3 -0
  104. package/lib/typescript/module/src/processor/default-processor.d.ts +6 -0
  105. package/lib/typescript/module/src/processor/session-processor.d.ts +11 -0
  106. package/lib/typescript/module/src/reporter/index.d.ts +18 -0
  107. package/lib/typescript/module/src/reporter/monitor-reporter.d.ts +8 -0
  108. package/lib/typescript/module/src/shell.d.ts +57 -0
  109. package/lib/typescript/module/src/types/client.d.ts +27 -0
  110. package/lib/typescript/module/src/types/crash.d.ts +95 -0
  111. package/lib/typescript/module/src/types/index.d.ts +118 -0
  112. package/lib/typescript/module/src/utils/action.d.ts +63 -0
  113. package/lib/typescript/module/src/utils/api.d.ts +59 -0
  114. package/lib/typescript/module/src/utils/base.d.ts +8 -0
  115. package/lib/typescript/module/src/utils/crash.d.ts +12 -0
  116. package/lib/typescript/module/src/utils/platform.d.ts +22 -0
  117. package/lib/typescript/module/src/utils/session.d.ts +41 -0
  118. package/lib/typescript/module/src/utils/url.d.ts +17 -0
  119. package/lib/typescript/module/src/utils/view.d.ts +13 -0
  120. package/package.json +51 -28
  121. package/lib/module/collector/action/index.js.map +0 -1
  122. package/lib/module/collector/exception/index.js.map +0 -1
  123. package/lib/module/collector/resource/api.js.map +0 -1
  124. package/lib/module/collector/view/perf.js.map +0 -1
  125. package/lib/module/collector/view/pv.js.map +0 -1
  126. package/lib/module/index.js.map +0 -1
  127. package/lib/module/plugin.js.map +0 -1
  128. package/lib/module/processor/default-processor.js.map +0 -1
  129. package/lib/module/processor/session-processor.js.map +0 -1
  130. package/lib/module/reporter/index.js.map +0 -1
  131. package/lib/module/shell.js.map +0 -1
  132. package/lib/module/types/client.js.map +0 -1
  133. package/lib/module/types/index.js.map +0 -1
  134. package/lib/module/utils/api.js.map +0 -1
  135. package/lib/module/utils/base.js.map +0 -1
  136. package/lib/module/utils/platform.js.map +0 -1
  137. package/lib/module/utils/session.js.map +0 -1
  138. package/lib/module/utils/url.js.map +0 -1
  139. package/lib/module/utils/view.js.map +0 -1
  140. package/lib/typescript/src/collector/action/index.d.ts.map +0 -1
  141. package/lib/typescript/src/collector/exception/index.d.ts.map +0 -1
  142. package/lib/typescript/src/collector/resource/api.d.ts.map +0 -1
  143. package/lib/typescript/src/collector/view/perf.d.ts.map +0 -1
  144. package/lib/typescript/src/collector/view/pv.d.ts.map +0 -1
  145. package/lib/typescript/src/index.d.ts +0 -4
  146. package/lib/typescript/src/index.d.ts.map +0 -1
  147. package/lib/typescript/src/plugin.d.ts.map +0 -1
  148. package/lib/typescript/src/processor/default-processor.d.ts.map +0 -1
  149. package/lib/typescript/src/processor/session-processor.d.ts.map +0 -1
  150. package/lib/typescript/src/reporter/index.d.ts.map +0 -1
  151. package/lib/typescript/src/shell.d.ts.map +0 -1
  152. package/lib/typescript/src/types/client.d.ts.map +0 -1
  153. package/lib/typescript/src/types/index.d.ts.map +0 -1
  154. package/lib/typescript/src/utils/api.d.ts.map +0 -1
  155. package/lib/typescript/src/utils/base.d.ts.map +0 -1
  156. package/lib/typescript/src/utils/platform.d.ts +0 -15
  157. package/lib/typescript/src/utils/platform.d.ts.map +0 -1
  158. package/lib/typescript/src/utils/session.d.ts.map +0 -1
  159. package/lib/typescript/src/utils/url.d.ts.map +0 -1
  160. package/lib/typescript/src/utils/view.d.ts.map +0 -1
  161. /package/lib/typescript/{src → commonjs/src}/collector/resource/api.d.ts +0 -0
  162. /package/lib/typescript/{src → commonjs/src}/collector/view/perf.d.ts +0 -0
  163. /package/lib/typescript/{src → commonjs/src}/collector/view/pv.d.ts +0 -0
  164. /package/lib/typescript/{src → commonjs/src}/plugin.d.ts +0 -0
  165. /package/lib/typescript/{src → commonjs/src}/processor/default-processor.d.ts +0 -0
  166. /package/lib/typescript/{src → commonjs/src}/types/client.d.ts +0 -0
  167. /package/lib/typescript/{src → commonjs/src}/types/index.d.ts +0 -0
  168. /package/lib/typescript/{src → commonjs/src}/utils/api.d.ts +0 -0
  169. /package/lib/typescript/{src → commonjs/src}/utils/base.d.ts +0 -0
  170. /package/lib/typescript/{src → commonjs/src}/utils/url.d.ts +0 -0
  171. /package/lib/typescript/{src → commonjs/src}/utils/view.d.ts +0 -0
  172. /package/lib/typescript/{package.json → module/package.json} +0 -0
@@ -0,0 +1,444 @@
1
+ package com.rumsdkreactnative.crash
2
+
3
+ import android.content.Context
4
+ import android.os.Build
5
+ import android.os.Looper
6
+ import org.json.JSONArray
7
+ import org.json.JSONObject
8
+ import java.io.File
9
+ import java.io.FileOutputStream
10
+ import java.util.UUID
11
+ import java.util.concurrent.locks.ReentrantLock
12
+
13
+ /**
14
+ * Java 层未捕获异常处理器。
15
+ *
16
+ * 关键设计:
17
+ * 1. 单例模式(@Volatile + double-check)
18
+ * 2. 自引用检测:避免重复 install 后形成无限递归链
19
+ * 3. ThreadLocal<Boolean> 重入保护:避免崩溃处理器自身抛异常时无限递归
20
+ * 4. ReentrantLock:多线程同时崩溃时只有一个进入采集流程
21
+ * 5. 同步落盘:FileOutputStream + fd.sync(),保证进程被杀死前文件已 flush 到磁盘
22
+ * 6. 链式调用:处理完成后调用前任 handler,保证系统/其他 SDK 行为不被破坏
23
+ */
24
+ class CrashHandler private constructor(
25
+ private val context: Context
26
+ ) : Thread.UncaughtExceptionHandler {
27
+
28
+ private val crashDir: File = File(context.filesDir, CRASH_DIR_NAME).apply {
29
+ // 迁移旧版目录 arms_crashes → rum_crashes
30
+ val legacyDir = File(context.filesDir, LEGACY_CRASH_DIR_NAME)
31
+ if (legacyDir.exists() && legacyDir.isDirectory) {
32
+ try {
33
+ if (!exists()) {
34
+ legacyDir.renameTo(this)
35
+ } else {
36
+ // 新目录已存在,逐个迁移文件
37
+ legacyDir.listFiles()?.forEach { file ->
38
+ val target = File(this, file.name)
39
+ if (!target.exists()) file.renameTo(target)
40
+ }
41
+ legacyDir.delete()
42
+ }
43
+ } catch (_: Throwable) {
44
+ // ignore migration errors
45
+ }
46
+ }
47
+ if (!exists()) {
48
+ try {
49
+ mkdirs()
50
+ } catch (_: Throwable) {
51
+ // ignore
52
+ }
53
+ }
54
+ }
55
+
56
+ private val contextManager: CrashContextManager = CrashContextManager(context)
57
+
58
+ /** 安装时保存的前任默认 handler(可能为系统/其他 SDK 安装的 handler) */
59
+ @Volatile
60
+ private var previousDefaultHandler: Thread.UncaughtExceptionHandler? = null
61
+
62
+ /** 安装时保存的主线程 handler(部分系统会单独设置) */
63
+ @Volatile
64
+ private var previousMainThreadHandler: Thread.UncaughtExceptionHandler? = null
65
+
66
+ @Volatile
67
+ private var installed: Boolean = false
68
+
69
+ /** 重入保护:处理崩溃过程中如果再次抛异常,直接转交前任 handler,避免死循环 */
70
+ private val reentrantGuard: ThreadLocal<Boolean> = object : ThreadLocal<Boolean>() {
71
+ override fun initialValue(): Boolean = false
72
+ }
73
+
74
+ /** 并发锁:多线程同时崩溃时只允许一个进入采集 */
75
+ private val lock = ReentrantLock()
76
+
77
+ /**
78
+ * 安装为默认 UncaughtExceptionHandler。
79
+ * 若已安装则直接返回,避免重复挂链导致死循环。
80
+ */
81
+ fun install() {
82
+ if (installed) return
83
+ synchronized(this) {
84
+ if (installed) return
85
+
86
+ val current = Thread.getDefaultUncaughtExceptionHandler()
87
+ // 自引用检测:若当前 default handler 已经是自己,跳过
88
+ if (current is CrashHandler) {
89
+ installed = true
90
+ return
91
+ }
92
+ previousDefaultHandler = current
93
+
94
+ val mainThread = Looper.getMainLooper().thread
95
+ val mainHandler = mainThread.uncaughtExceptionHandler
96
+ if (mainHandler !== current && mainHandler !is CrashHandler) {
97
+ previousMainThreadHandler = mainHandler
98
+ }
99
+
100
+ Thread.setDefaultUncaughtExceptionHandler(this)
101
+ installed = true
102
+
103
+ // 安装 NDK 信号处理器,捕获原生崩溃(SIGSEGV / SIGABRT 等)
104
+ try {
105
+ NdkCrashHandler.install(crashDir.absolutePath)
106
+ } catch (_: Throwable) {
107
+ // 忽略 NDK 安装失败,不影响 Java 层 handler
108
+ }
109
+ }
110
+ }
111
+
112
+ override fun uncaughtException(thread: Thread, throwable: Throwable) {
113
+ // 重入保护
114
+ if (reentrantGuard.get() == true) {
115
+ chainToPrevious(thread, throwable)
116
+ return
117
+ }
118
+ reentrantGuard.set(true)
119
+
120
+ val acquired = try {
121
+ lock.tryLock()
122
+ } catch (_: Throwable) {
123
+ false
124
+ }
125
+
126
+ try {
127
+ if (acquired) {
128
+ try {
129
+ captureAndPersist(thread, throwable)
130
+ } catch (_: Throwable) {
131
+ // 崩溃处理器本身不能抛异常
132
+ }
133
+ }
134
+ // 未获取到锁说明已有线程在处理,直接走链式调用即可
135
+ } finally {
136
+ if (acquired) {
137
+ try {
138
+ lock.unlock()
139
+ } catch (_: Throwable) {
140
+ // ignore
141
+ }
142
+ }
143
+ try {
144
+ reentrantGuard.set(false)
145
+ } catch (_: Throwable) {
146
+ // ignore
147
+ }
148
+ chainToPrevious(thread, throwable)
149
+ }
150
+ }
151
+
152
+ private fun chainToPrevious(thread: Thread, throwable: Throwable) {
153
+ try {
154
+ // 主线程优先使用 previousMainThreadHandler
155
+ if (thread == Looper.getMainLooper().thread &&
156
+ previousMainThreadHandler != null
157
+ ) {
158
+ previousMainThreadHandler?.uncaughtException(thread, throwable)
159
+ } else {
160
+ previousDefaultHandler?.uncaughtException(thread, throwable)
161
+ }
162
+ } catch (_: Throwable) {
163
+ // ignore
164
+ }
165
+ }
166
+
167
+ private fun captureAndPersist(thread: Thread, throwable: Throwable) {
168
+ val crashId = UUID.randomUUID().toString()
169
+ val timestamp = System.currentTimeMillis()
170
+
171
+ val root = JSONObject()
172
+ root.put("crash_id", crashId)
173
+ root.put("timestamp", timestamp)
174
+ root.put("platform", "android")
175
+ root.put("exception", buildException(throwable))
176
+ root.put("crashed_thread", buildThread(thread, throwable.stackTrace))
177
+ root.put("context", safeReadContext())
178
+ root.put("device", buildDevice())
179
+ root.put("app", buildApp(context))
180
+
181
+ writeCrashFile(crashId, timestamp, root.toString())
182
+ }
183
+
184
+ private fun buildException(throwable: Throwable): JSONObject {
185
+ val obj = JSONObject()
186
+ obj.put("type", "java_exception")
187
+ obj.put("class_name", throwable.javaClass.name)
188
+ obj.put("message", throwable.message ?: "")
189
+ // 遍历 cause 链(最多 5 层),提取完整异常因果链
190
+ val causes = JSONArray()
191
+ var current = throwable.cause
192
+ var depth = 0
193
+ while (current != null && current !== throwable && depth < 5) {
194
+ try {
195
+ val causeObj = JSONObject()
196
+ causeObj.put("class_name", current.javaClass.name)
197
+ causeObj.put("message", current.message ?: "")
198
+ causes.put(causeObj)
199
+ } catch (_: Throwable) {
200
+ // ignore single cause errors
201
+ }
202
+ current = current.cause
203
+ depth++
204
+ }
205
+ if (causes.length() > 0) {
206
+ obj.put("causes", causes)
207
+ }
208
+ return obj
209
+ }
210
+
211
+ private fun buildThread(thread: Thread, stackTrace: Array<StackTraceElement>?): JSONObject {
212
+ val obj = JSONObject()
213
+ obj.put("id", thread.id.toString())
214
+ obj.put("name", thread.name ?: "")
215
+ obj.put("frames", buildFrames(stackTrace))
216
+ return obj
217
+ }
218
+
219
+ private fun buildFrames(stackTrace: Array<StackTraceElement>?): JSONArray {
220
+ val arr = JSONArray()
221
+ if (stackTrace == null) return arr
222
+ // 双侧截断(参照 Sentry 推荐方案):帧数超过 512 时保留首尾各 256 帧,
223
+ // 中间插入标记帧,防止递归崩溃(如 StackOverflowError 产生数千帧)丢失程序入口点。
224
+ val truncationThreshold = 512
225
+ val keepPerSide = 256
226
+ val total = stackTrace.size
227
+ if (total > truncationThreshold) {
228
+ // 前 keepPerSide 帧
229
+ appendFrameRange(arr, stackTrace, 0, keepPerSide)
230
+ // 截断标记帧
231
+ try {
232
+ val marker = JSONObject()
233
+ marker.put("truncated", true)
234
+ marker.put("omitted_count", total - truncationThreshold)
235
+ arr.put(marker)
236
+ } catch (_: Throwable) {
237
+ // ignore
238
+ }
239
+ // 后 keepPerSide 帧
240
+ appendFrameRange(arr, stackTrace, total - keepPerSide, total)
241
+ } else {
242
+ // 帧数未超阈值,全部保留
243
+ appendFrameRange(arr, stackTrace, 0, total)
244
+ }
245
+ return arr
246
+ }
247
+
248
+ private fun appendFrameRange(
249
+ arr: JSONArray,
250
+ stackTrace: Array<StackTraceElement>,
251
+ from: Int,
252
+ to: Int
253
+ ) {
254
+ for (i in from until to) {
255
+ try {
256
+ val element = stackTrace[i]
257
+ val frame = JSONObject()
258
+ frame.put("class_name", element.className ?: "")
259
+ frame.put("method_name", element.methodName ?: "")
260
+ frame.put("file", element.fileName ?: "")
261
+ frame.put("line", element.lineNumber)
262
+ arr.put(frame)
263
+ } catch (_: Throwable) {
264
+ // ignore single frame errors
265
+ }
266
+ }
267
+ }
268
+
269
+ private fun safeReadContext(): JSONObject {
270
+ return try {
271
+ contextManager.readContext() ?: JSONObject()
272
+ } catch (_: Throwable) {
273
+ JSONObject()
274
+ }
275
+ }
276
+
277
+ /**
278
+ * 同步落盘:FileOutputStream + fd.sync()。
279
+ * 文件命名:crash_{timestamp}_{uuid}.json
280
+ */
281
+ private fun writeCrashFile(crashId: String, timestamp: Long, json: String) {
282
+ if (!crashDir.exists()) {
283
+ try {
284
+ crashDir.mkdirs()
285
+ } catch (_: Throwable) {
286
+ // ignore
287
+ }
288
+ }
289
+ val target = File(crashDir, "crash_${timestamp}_${crashId}.json")
290
+ var fos: FileOutputStream? = null
291
+ try {
292
+ fos = FileOutputStream(target)
293
+ fos.write(json.toByteArray(Charsets.UTF_8))
294
+ fos.flush()
295
+ try {
296
+ fos.fd.sync()
297
+ } catch (_: Throwable) {
298
+ // ignore sync errors,已尽力
299
+ }
300
+ } catch (_: Throwable) {
301
+ // ignore
302
+ } finally {
303
+ try {
304
+ fos?.close()
305
+ } catch (_: Throwable) {
306
+ // ignore
307
+ }
308
+ }
309
+ }
310
+
311
+ companion object {
312
+ private const val CRASH_DIR_NAME = "rum_crashes"
313
+ private const val LEGACY_CRASH_DIR_NAME = "arms_crashes"
314
+
315
+ /**
316
+ * 构建设备信息 JSON(供 CrashHandler 和 RumSdkReactnativeModule 共享)。
317
+ * NDK 崩溃的信号处理器只写 crash-specific 数据,不包含 device 字段,
318
+ * getPendingCrashes 读取时需要补入。
319
+ */
320
+ fun buildDevice(): JSONObject {
321
+ val obj = JSONObject()
322
+ try {
323
+ obj.put("model", Build.MODEL ?: "")
324
+ obj.put("brand", Build.MANUFACTURER?.takeIf { it.isNotBlank() } ?: (Build.BRAND ?: ""))
325
+ obj.put("os_version", Build.VERSION.RELEASE ?: "")
326
+ } catch (_: Throwable) {
327
+ // ignore
328
+ }
329
+ return obj
330
+ }
331
+
332
+ /**
333
+ * 构建应用信息 JSON(供 CrashHandler 和 RumSdkReactnativeModule 共享)。
334
+ * NDK 崩溃的信号处理器只写 crash-specific 数据,不包含 app 字段,
335
+ * getPendingCrashes 读取时需要补入。
336
+ */
337
+ fun buildApp(context: Context): JSONObject {
338
+ val obj = JSONObject()
339
+ try {
340
+ obj.put("package", context.packageName ?: "")
341
+ val pm = context.packageManager
342
+ val info = pm.getPackageInfo(context.packageName, 0)
343
+ obj.put("version", info.versionName ?: "")
344
+ val build = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
345
+ info.longVersionCode.toString()
346
+ } else {
347
+ @Suppress("DEPRECATION")
348
+ info.versionCode.toString()
349
+ }
350
+ obj.put("build", build)
351
+ // 补入 React Native 版本号,与 iOS 对齐
352
+ try {
353
+ val versionClass = Class.forName("com.facebook.react.modules.systeminfo.ReactNativeVersion")
354
+ val versionField = versionClass.getDeclaredField("VERSION")
355
+ @Suppress("UNCHECKED_CAST")
356
+ val version = versionField.get(null) as? Map<*, *>
357
+ val rnVersion = "${version?.get("major") ?: 0}.${version?.get("minor") ?: 0}.${version?.get("patch") ?: 0}"
358
+ obj.put("rn_version", rnVersion)
359
+ } catch (_: Throwable) {
360
+ // RN 版本不可用时跳过
361
+ }
362
+ } catch (_: Throwable) {
363
+ // ignore
364
+ }
365
+ return obj
366
+ }
367
+
368
+ /**
369
+ * 为 NDK 崩溃补全缺失的标准字段(platform / exception / crashed_thread / mechanism)。
370
+ *
371
+ * NDK 信号处理器(rum_crash_writer.c)在 async-signal-safe 上下文中只写入了
372
+ * crash-specific 数据(signal / frames / arch / binary_images 等),
373
+ * 缺少 platform、exception、crashed_thread、mechanism 等结构化字段。
374
+ * 此方法在 getPendingCrashes 读取后统一补入,使 NDK 崩溃与 Java 崩溃
375
+ * 具有一致的数据结构,便于服务端解析和前端展示。
376
+ *
377
+ * 所有补入均使用 `if (!json.has(...))` 条件判断,不会覆盖已有字段。
378
+ */
379
+ fun enrichNdkCrash(json: JSONObject) {
380
+ // 仅处理 NDK 崩溃(有 signal 字段),Java 崩溃已有完整结构
381
+ if (!json.has("signal")) return
382
+ try {
383
+ // 补入 platform 字段,与 Java 崩溃保持一致
384
+ if (!json.has("platform")) {
385
+ json.put("platform", "android")
386
+ }
387
+
388
+ // 补入 exception 结构
389
+ if (!json.has("exception")) {
390
+ val signal = json.optJSONObject("signal")
391
+ val signalName = signal?.optString("name", "unknown") ?: "unknown"
392
+ val faultAddr = signal?.optString("fault_address", "0x0") ?: "0x0"
393
+ val exception = JSONObject()
394
+ exception.put("type", "native_crash")
395
+ exception.put("class_name", signalName)
396
+ exception.put("message", "$signalName at $faultAddr")
397
+ json.put("exception", exception)
398
+ }
399
+
400
+ // NDK frames 在 JSON 顶层,重组为 crashed_thread 结构
401
+ if (json.has("frames") && !json.has("crashed_thread")) {
402
+ val crashedThread = JSONObject()
403
+ crashedThread.put("id", json.optInt("tid", 0))
404
+ crashedThread.put("frames", json.get("frames"))
405
+ json.put("crashed_thread", crashedThread)
406
+ json.remove("frames")
407
+ // tid 保留在顶层,JS 层也用它做 fallback
408
+ }
409
+
410
+ // 补入 mechanism 信息(JS 层会将其放入 snapshots)
411
+ if (!json.has("mechanism")) {
412
+ val signal = json.optJSONObject("signal")
413
+ val signalName = signal?.optString("name", "signal") ?: "signal"
414
+ val mechanism = JSONObject()
415
+ mechanism.put("type", "signal")
416
+ mechanism.put("handled", false)
417
+ mechanism.put("signal_name", signalName)
418
+ json.put("mechanism", mechanism)
419
+ }
420
+ } catch (_: Throwable) {
421
+ // 补入失败不影响已有数据
422
+ }
423
+ }
424
+
425
+ @Volatile
426
+ private var instance: CrashHandler? = null
427
+
428
+ @JvmStatic
429
+ fun getInstance(context: Context): CrashHandler {
430
+ val existing = instance
431
+ if (existing != null) return existing
432
+ synchronized(CrashHandler::class.java) {
433
+ val again = instance
434
+ if (again != null) return again
435
+ val created = CrashHandler(context.applicationContext)
436
+ instance = created
437
+ return created
438
+ }
439
+ }
440
+
441
+ @JvmStatic
442
+ fun peekInstance(): CrashHandler? = instance
443
+ }
444
+ }
@@ -0,0 +1,50 @@
1
+ package com.rumsdkreactnative.crash
2
+
3
+ import android.content.ContentProvider
4
+ import android.content.ContentValues
5
+ import android.database.Cursor
6
+ import android.net.Uri
7
+
8
+ /**
9
+ * 通过 ContentProvider 实现无侵入自启动。
10
+ *
11
+ * Android 在 Application.onCreate() 之前会调用所有已声明 ContentProvider 的 onCreate(),
12
+ * 因此这里是接入崩溃处理器最早的、且无需用户在 Application 中显式调用的时机。
13
+ *
14
+ * 注意:onCreate() 必须保持轻量,且绝不允许抛异常。
15
+ */
16
+ class CrashInitProvider : ContentProvider() {
17
+
18
+ override fun onCreate(): Boolean {
19
+ try {
20
+ val ctx = context
21
+ if (ctx != null) {
22
+ CrashHandler.getInstance(ctx.applicationContext).install()
23
+ }
24
+ } catch (_: Throwable) {
25
+ // 自启动过程任何异常都不能抛出,避免影响宿主 App 启动
26
+ }
27
+ return true
28
+ }
29
+
30
+ override fun query(
31
+ uri: Uri,
32
+ projection: Array<out String>?,
33
+ selection: String?,
34
+ selectionArgs: Array<out String>?,
35
+ sortOrder: String?
36
+ ): Cursor? = null
37
+
38
+ override fun getType(uri: Uri): String? = null
39
+
40
+ override fun insert(uri: Uri, values: ContentValues?): Uri? = null
41
+
42
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
43
+
44
+ override fun update(
45
+ uri: Uri,
46
+ values: ContentValues?,
47
+ selection: String?,
48
+ selectionArgs: Array<out String>?
49
+ ): Int = 0
50
+ }
@@ -0,0 +1,66 @@
1
+ package com.rumsdkreactnative.crash
2
+
3
+ import android.util.Log
4
+
5
+ /**
6
+ * JNI bridge for the native (NDK) crash signal handler.
7
+ *
8
+ * Loads librum_crash.so and installs sigaction-based handlers for
9
+ * SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL/SIGTRAP. Crash dumps are written
10
+ * to the provided directory as `crash_native_{timestamp}.json`.
11
+ *
12
+ * Designed to fail soft: if the native library cannot be loaded
13
+ * (e.g. unsupported ABI), the install is a no-op and the SDK falls
14
+ * back to Java-only crash capture.
15
+ */
16
+ object NdkCrashHandler {
17
+
18
+ private const val TAG = "RumCrash"
19
+ private const val LIB_NAME = "rum_crash"
20
+
21
+ @Volatile
22
+ private var installed = false
23
+
24
+ @Volatile
25
+ private var libraryLoaded = false
26
+
27
+ init {
28
+ try {
29
+ System.loadLibrary(LIB_NAME)
30
+ libraryLoaded = true
31
+ } catch (e: UnsatisfiedLinkError) {
32
+ Log.w(TAG, "Failed to load lib$LIB_NAME.so: ${e.message}")
33
+ } catch (e: SecurityException) {
34
+ Log.w(TAG, "SecurityException loading lib$LIB_NAME.so: ${e.message}")
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Install the native signal handlers. Idempotent and thread-safe.
40
+ *
41
+ * @param crashDir absolute directory where crash JSON files will be written.
42
+ * Must already exist and be writable by the app process.
43
+ */
44
+ @Synchronized
45
+ fun install(crashDir: String) {
46
+ if (installed) return
47
+ if (!libraryLoaded) {
48
+ Log.w(TAG, "Native lib not loaded; NDK crash capture disabled")
49
+ return
50
+ }
51
+ try {
52
+ nativeInstall(crashDir)
53
+ installed = true
54
+ Log.i(TAG, "NDK crash handler installed -> $crashDir")
55
+ } catch (e: UnsatisfiedLinkError) {
56
+ Log.w(TAG, "nativeInstall not found: ${e.message}")
57
+ } catch (e: Throwable) {
58
+ Log.w(TAG, "nativeInstall failed: ${e.message}")
59
+ }
60
+ }
61
+
62
+ fun isInstalled(): Boolean = installed
63
+
64
+ @JvmStatic
65
+ private external fun nativeInstall(crashDir: String)
66
+ }
@@ -0,0 +1,21 @@
1
+ cmake_minimum_required(VERSION 3.22.1)
2
+ project(rum_crash)
3
+
4
+ add_library(rum_crash SHARED
5
+ rum_crash_handler.c
6
+ rum_backtrace.c
7
+ rum_crash_writer.c
8
+ rum_module_info.c
9
+ rum_test_crash.cpp
10
+ )
11
+
12
+ target_link_libraries(rum_crash
13
+ log
14
+ android
15
+ )
16
+
17
+ target_compile_options(rum_crash PRIVATE
18
+ -fno-omit-frame-pointer
19
+ -Wno-format
20
+ -Wno-unused-parameter
21
+ )