@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,512 @@
1
+ package com.rumsdkreactnative
2
+
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+ import android.content.res.Configuration
6
+ import android.os.Build
7
+ import android.provider.Settings
8
+ import android.util.DisplayMetrics
9
+ import android.view.WindowManager
10
+ import com.facebook.react.bridge.Arguments
11
+ import com.facebook.react.bridge.Promise
12
+ import com.facebook.react.bridge.ReactApplicationContext
13
+ import com.facebook.react.module.annotations.ReactModule
14
+ import com.rumsdkreactnative.crash.CrashContextManager
15
+ import com.rumsdkreactnative.crash.CrashHandler
16
+ import org.json.JSONArray
17
+ import org.json.JSONObject
18
+ import java.io.File
19
+
20
+ @ReactModule(name = RumSdkReactnativeModule.NAME)
21
+ class RumSdkReactnativeModule(reactContext: ReactApplicationContext) :
22
+ NativeRumSdkReactnativeSpec(reactContext) {
23
+
24
+ private val prefs: SharedPreferences =
25
+ migratePrefs(reactContext).also {
26
+ // 使用迁移后的 prefs
27
+ }
28
+
29
+ private val crashDir: File =
30
+ File(reactContext.applicationContext.filesDir, CRASH_DIR_NAME)
31
+
32
+ private val crashContextManager: CrashContextManager =
33
+ CrashContextManager(reactContext.applicationContext)
34
+
35
+ override fun getName(): String = NAME
36
+
37
+ // region Storage
38
+
39
+ override fun getItem(key: String, promise: Promise) {
40
+ try {
41
+ promise.resolve(prefs.getString(key, null))
42
+ } catch (e: Throwable) {
43
+ promise.reject(ERR_STORAGE, e)
44
+ }
45
+ }
46
+
47
+ override fun setItem(key: String, value: String, promise: Promise) {
48
+ try {
49
+ prefs.edit().putString(key, value).apply()
50
+ promise.resolve(null)
51
+ } catch (e: Throwable) {
52
+ promise.reject(ERR_STORAGE, e)
53
+ }
54
+ }
55
+
56
+ override fun removeItem(key: String, promise: Promise) {
57
+ try {
58
+ prefs.edit().remove(key).apply()
59
+ promise.resolve(null)
60
+ } catch (e: Throwable) {
61
+ promise.reject(ERR_STORAGE, e)
62
+ }
63
+ }
64
+
65
+ override fun clear(promise: Promise) {
66
+ try {
67
+ prefs.edit().clear().apply()
68
+ promise.resolve(null)
69
+ } catch (e: Throwable) {
70
+ promise.reject(ERR_STORAGE, e)
71
+ }
72
+ }
73
+
74
+ override fun getAllKeys(promise: Promise) {
75
+ try {
76
+ val array = Arguments.createArray()
77
+ for (key in prefs.all.keys) {
78
+ array.pushString(key)
79
+ }
80
+ promise.resolve(array)
81
+ } catch (e: Throwable) {
82
+ promise.reject(ERR_STORAGE, e)
83
+ }
84
+ }
85
+
86
+ // endregion
87
+
88
+ // region Device Info
89
+
90
+ override fun getDeviceInfo(promise: Promise) {
91
+ try {
92
+ val ctx = reactApplicationContext
93
+ val map = Arguments.createMap()
94
+
95
+ // device.id - 使用 ANDROID_ID,设备级唯一标识(恢复出厂设置后重置)
96
+ try {
97
+ val androidId = Settings.Secure.getString(
98
+ ctx.contentResolver,
99
+ Settings.Secure.ANDROID_ID
100
+ )
101
+ if (!androidId.isNullOrBlank()) {
102
+ map.putString("device_id", androidId)
103
+ }
104
+ } catch (_: Throwable) {
105
+ // 部分厂商可能限制访问,忽略
106
+ }
107
+
108
+ // device.brand - 设备品牌,优先 MANUFACTURER(如 "Xiaomi"),BRAND 作为兜底
109
+ val brand = Build.MANUFACTURER?.takeIf { it.isNotBlank() } ?: Build.BRAND ?: ""
110
+ map.putString("device_brand", brand)
111
+
112
+ // device.model - 设备型号
113
+ map.putString("device_model", Build.MODEL ?: "")
114
+
115
+ // device.name - 设备名称(工业名称,如 "goldfish")
116
+ map.putString("device_name", Build.DEVICE ?: "")
117
+
118
+ // device.type - 根据屏幕尺寸类别判断 mobile / tablet
119
+ val screenLayout = ctx.resources.configuration.screenLayout
120
+ val screenSize = screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK
121
+ val deviceType = when {
122
+ screenSize >= Configuration.SCREENLAYOUT_SIZE_LARGE -> "tablet"
123
+ else -> "mobile"
124
+ }
125
+ map.putString("device_type", deviceType)
126
+
127
+ // os.type - 操作系统
128
+ map.putString("os_type", "Android")
129
+
130
+ // os.version - 操作系统版本
131
+ map.putString("os_version", Build.VERSION.RELEASE ?: "")
132
+
133
+ // app.package - 应用包名
134
+ map.putString("app_package", ctx.packageName ?: "")
135
+
136
+ // app.version - 应用版本(versionName)
137
+ try {
138
+ val packageInfo = ctx.packageManager.getPackageInfo(ctx.packageName, 0)
139
+ map.putString("app_version", packageInfo.versionName ?: "")
140
+ } catch (_: Throwable) {
141
+ // 忽略
142
+ }
143
+
144
+ // viewport - 当前屏幕像素(物理像素)
145
+ try {
146
+ val wm = ctx.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
147
+ val metrics = DisplayMetrics()
148
+ @Suppress("DEPRECATION")
149
+ wm?.defaultDisplay?.getRealMetrics(metrics)
150
+ if (metrics.widthPixels > 0) {
151
+ map.putInt("viewport_width", metrics.widthPixels)
152
+ map.putInt("viewport_height", metrics.heightPixels)
153
+ }
154
+ } catch (_: Throwable) {
155
+ // 忽略
156
+ }
157
+
158
+ promise.resolve(map)
159
+ } catch (e: Throwable) {
160
+ promise.reject(ERR_DEVICE_INFO, e)
161
+ }
162
+ }
163
+
164
+ // endregion
165
+
166
+ // region Crash
167
+
168
+ /**
169
+ * 读取 crashDir 下所有崩溃报告文件,组装为 JSONArray 字符串返回。
170
+ * 崩溃文件命名:crash_{timestamp}_{id}.json(Java)或 crash_native_{timestamp}_{pid}.json(NDK)
171
+ * 注意:排除 context 文件(.rum_crash_context)和临时文件
172
+ */
173
+ override fun getPendingCrashes(promise: Promise) {
174
+ try {
175
+ val arr = JSONArray()
176
+ if (crashDir.exists() && crashDir.isDirectory) {
177
+ // NDK 崩溃的信号处理器只写 crash-specific 数据(signal/frames/arch 等),
178
+ // 不包含 context/device/app 字段。Java 崩溃在 captureAndPersist 中已写入这些字段。
179
+ // 在此处统一读取 context 文件和构建设备/应用信息,为缺少这些字段的 NDK 崩溃补入。
180
+ val crashContext = try {
181
+ crashContextManager.readContext() ?: JSONObject()
182
+ } catch (_: Throwable) {
183
+ JSONObject()
184
+ }
185
+ val deviceInfo = try {
186
+ CrashHandler.buildDevice()
187
+ } catch (_: Throwable) {
188
+ JSONObject()
189
+ }
190
+ val appInfo = try {
191
+ CrashHandler.buildApp(reactApplicationContext.applicationContext)
192
+ } catch (_: Throwable) {
193
+ JSONObject()
194
+ }
195
+
196
+ val files = crashDir.listFiles { f ->
197
+ isCrashReportFile(f.name)
198
+ }
199
+ files?.sortedBy { it.lastModified() }?.forEach { file ->
200
+ try {
201
+ val text = file.readText(Charsets.UTF_8)
202
+ if (text.isNotEmpty()) {
203
+ // 解析为 JSONObject 后放入数组,确保 JS 层 JSON.parse 后获得对象数组。
204
+ // NDK 崩溃写入器在 binary_images 循环中发生二次崩溃时,核心崩溃数据
205
+ // (crash_id/timestamp/signal/frames)虽已落盘但 JSON 不完整(缺少
206
+ // 闭合 ]}),JSONObject(text) 会抛异常导致核心数据完全丢失。
207
+ // 尝试修复截断的 JSON 以恢复核心数据。
208
+ val json = try {
209
+ JSONObject(text)
210
+ } catch (_: Throwable) {
211
+ val repaired = repairTruncatedJson(text)
212
+ if (repaired != null) {
213
+ try { JSONObject(repaired) } catch (_: Throwable) { null }
214
+ } else null
215
+ }
216
+ if (json != null) {
217
+ // 补全 NDK 崩溃缺失的 context/device/app 字段。
218
+ // 仅在字段不存在时补入,避免覆盖 Java 崩溃已有的数据。
219
+ if (!json.has("context")) {
220
+ json.put("context", crashContext)
221
+ }
222
+ if (!json.has("device")) {
223
+ json.put("device", deviceInfo)
224
+ }
225
+ if (!json.has("app")) {
226
+ json.put("app", appInfo)
227
+ }
228
+ // 为 NDK 崩溃补全 platform/exception/crashed_thread/mechanism 等结构化字段
229
+ CrashHandler.enrichNdkCrash(json)
230
+ arr.put(json)
231
+ }
232
+ }
233
+ } catch (_: Throwable) {
234
+ // ignore single file
235
+ }
236
+ }
237
+ }
238
+ promise.resolve(arr.toString())
239
+ } catch (e: Throwable) {
240
+ promise.reject(ERR_CRASH, e)
241
+ }
242
+ }
243
+
244
+ /**
245
+ * 解析 crashIds JSON array,按 crash_id 删除对应文件。
246
+ */
247
+ override fun clearCrashReports(crashIds: String, promise: Promise) {
248
+ try {
249
+ val ids = HashSet<String>()
250
+ if (crashIds.isNotEmpty()) {
251
+ try {
252
+ val parsed = JSONArray(crashIds)
253
+ for (i in 0 until parsed.length()) {
254
+ val id = parsed.optString(i, null) ?: continue
255
+ if (id.isNotEmpty()) ids.add(id)
256
+ }
257
+ } catch (_: Throwable) {
258
+ // ignore parse error
259
+ }
260
+ }
261
+ if (ids.isNotEmpty() && crashDir.exists() && crashDir.isDirectory) {
262
+ val files = crashDir.listFiles { f ->
263
+ isCrashReportFile(f.name)
264
+ }
265
+ files?.forEach { file ->
266
+ try {
267
+ // 策略 1:通过文件名匹配(Java 崩溃 + 修复后的 NDK 崩溃)
268
+ // crash_id 现在为 crash_native_{ts}_{pid},与文件名 nameWithoutExt 完全一致
269
+ var matched = false
270
+ for (id in ids) {
271
+ val nameWithoutExt = file.nameWithoutExtension
272
+ if (nameWithoutExt.endsWith("_$id") || nameWithoutExt == id) {
273
+ file.delete()
274
+ matched = true
275
+ break
276
+ }
277
+ }
278
+ if (matched) return@forEach
279
+
280
+ // 策略 2:通过文件内容中的 crash_id 字段匹配(历史遗留 NDK 崩溃)
281
+ // 旧版 NDK 崩溃文件名为 crash_native_{ts}_{pid},但 crash_id 为 ndk_{ts}_{pid},
282
+ // 文件名匹配失败。读取文件 JSON 中的 crash_id 进行精确匹配。
283
+ val text = file.readText(Charsets.UTF_8)
284
+ if (text.isNotEmpty()) {
285
+ val json = try { JSONObject(text) } catch (_: Throwable) {
286
+ val repaired = repairTruncatedJson(text)
287
+ if (repaired != null) {
288
+ try { JSONObject(repaired) } catch (_: Throwable) { null }
289
+ } else null
290
+ }
291
+ if (json != null) {
292
+ val crashId = json.optString("crash_id", "")
293
+ if (crashId.isNotEmpty() && ids.contains(crashId)) {
294
+ file.delete()
295
+ }
296
+ }
297
+ }
298
+ } catch (_: Throwable) {
299
+ // ignore
300
+ }
301
+ }
302
+ }
303
+ promise.resolve(null)
304
+ } catch (e: Throwable) {
305
+ promise.reject(ERR_CRASH, e)
306
+ }
307
+ }
308
+
309
+ /** 上次运行是否发生过崩溃:crashDir 中存在崩溃报告文件即认为发生过 */
310
+ override fun crashedLastRun(promise: Promise) {
311
+ try {
312
+ val crashed = crashDir.exists() &&
313
+ (crashDir.listFiles()?.any { it.isFile && isCrashReportFile(it.name) } ?: false)
314
+ promise.resolve(crashed)
315
+ } catch (e: Throwable) {
316
+ promise.reject(ERR_CRASH, e)
317
+ }
318
+ }
319
+
320
+ /** 更新崩溃上下文快照(session/view 变更时调用) */
321
+ override fun updateCrashContext(context: String) {
322
+ try {
323
+ crashContextManager.updateContext(context)
324
+ } catch (_: Throwable) {
325
+ // ignore
326
+ }
327
+ }
328
+
329
+ /** 清除所有崩溃报告文件(用于清理无 crash_id 的旧版 NDK 文件) */
330
+ override fun clearAllCrashReports(promise: Promise) {
331
+ try {
332
+ if (crashDir.exists() && crashDir.isDirectory) {
333
+ val files = crashDir.listFiles { f -> isCrashReportFile(f.name) }
334
+ files?.forEach { file ->
335
+ try {
336
+ file.delete()
337
+ } catch (_: Throwable) {
338
+ // ignore
339
+ }
340
+ }
341
+ }
342
+ promise.resolve(null)
343
+ } catch (e: Throwable) {
344
+ promise.reject(ERR_CRASH, e)
345
+ }
346
+ }
347
+
348
+ /** 清理过期和超限的崩溃文件 */
349
+ override fun cleanupOldCrashes(maxFiles: Double, maxAgeMs: Double, promise: Promise) {
350
+ try {
351
+ if (!crashDir.exists() || !crashDir.isDirectory) {
352
+ promise.resolve(null)
353
+ return
354
+ }
355
+
356
+ val maxFileCount = maxFiles.toInt().coerceAtLeast(1)
357
+ val maxAge = maxAgeMs.toLong()
358
+ val now = System.currentTimeMillis()
359
+
360
+ val files = crashDir.listFiles { f -> isCrashReportFile(f.name) }
361
+ ?.sortedByDescending { it.lastModified() }
362
+ ?: emptyList()
363
+
364
+ // 1. 删除过期文件
365
+ val remaining = files.filter { file ->
366
+ val expired = now - file.lastModified() > maxAge
367
+ if (expired) {
368
+ try { file.delete() } catch (_: Throwable) {}
369
+ }
370
+ !expired
371
+ }
372
+
373
+ // 2. 超过上限时删除最旧的
374
+ if (remaining.size > maxFileCount) {
375
+ remaining.drop(maxFileCount).forEach { file ->
376
+ try { file.delete() } catch (_: Throwable) {}
377
+ }
378
+ }
379
+
380
+ promise.resolve(null)
381
+ } catch (e: Throwable) {
382
+ promise.reject(ERR_CRASH, e)
383
+ }
384
+ }
385
+
386
+ /**
387
+ * 尝试修复截断的 JSON 文本。
388
+ * NDK 崩溃写入器在写入过程中发生二次崩溃时,落盘的 JSON 可能缺少闭合括号。
389
+ * 本方法去除末尾多余逗号,统计未闭合的 { 和 [(正确处理字符串内的引号转义),
390
+ * 追加对应数量的 ] 和 } 闭合字符。
391
+ *
392
+ * @return 修复后的 JSON 字符串;如果 JSON 已完整或无法修复则返回 null
393
+ */
394
+ private fun repairTruncatedJson(text: String): String? {
395
+ var s = text.trimEnd()
396
+ // 去除末尾逗号
397
+ while (s.endsWith(",")) s = s.dropLast(1)
398
+ // 统计未闭合的括号
399
+ var braces = 0
400
+ var brackets = 0
401
+ var inStr = false
402
+ var esc = false
403
+ for (c in s) {
404
+ if (esc) { esc = false; continue }
405
+ if (c == '\\') { esc = true; continue }
406
+ if (c == '"') { inStr = !inStr; continue }
407
+ if (inStr) continue
408
+ when (c) {
409
+ '{' -> braces++
410
+ '}' -> braces--
411
+ '[' -> brackets++
412
+ ']' -> brackets--
413
+ }
414
+ }
415
+ if (brackets < 0 || braces < 0) return null
416
+ if (brackets == 0 && braces == 0) return null // JSON 已经完整,不需要修复
417
+ // 若截断发生在字符串值内部(inStr == true),需先补上闭合引号,
418
+ // 否则追加 ]} 后仍是非法 JSON,JSONObject(repaired) 会抛异常导致核心崩溃数据丢失。
419
+ val prefix = if (inStr) "\"" else ""
420
+ return prefix + s + "]".repeat(brackets) + "}".repeat(braces)
421
+ }
422
+
423
+ // endregion
424
+
425
+ // region Test Crash
426
+
427
+ private external fun nativeTriggerCrash(type: Int)
428
+
429
+ override fun triggerTestCrash(crashType: String) {
430
+ when (crashType) {
431
+ // TurboModule 架构下,@ReactMethod void 方法中抛出的 Java 异常会被 C++ 层
432
+ // 拦截并转换为 JS 异常,不会到达 Thread.getDefaultUncaughtExceptionHandler()。
433
+ // 在新线程中抛出异常,可正确触发 CrashHandler 捕获并写入崩溃文件。
434
+ "java_exception" -> {
435
+ Thread {
436
+ throw RuntimeException("RUM SDK Test: Uncaught Java Exception")
437
+ }.apply { name = "rum-test-crash" }.start()
438
+ // 给新线程足够时间触发 UncaughtExceptionHandler,确保崩溃先于
439
+ // 方法返回触发,E2E 测试才能正确捕获 reject。
440
+ Thread.sleep(100)
441
+ }
442
+ "sigsegv" -> nativeTriggerCrash(0)
443
+ "sigabrt" -> nativeTriggerCrash(1)
444
+ "sigfpe" -> nativeTriggerCrash(2)
445
+ "cpp_exception" -> nativeTriggerCrash(3)
446
+ "stack_overflow" -> nativeTriggerCrash(4)
447
+ }
448
+ }
449
+
450
+ // endregion
451
+
452
+ companion object {
453
+ const val NAME = "RumSdkReactnative"
454
+ private const val PREFS_NAME = "com.aliyun.rum.reactnative"
455
+ private const val LEGACY_PREFS_NAME = "com.aliyun.arms.rum.reactnative"
456
+ private const val CRASH_DIR_NAME = "rum_crashes"
457
+ private const val ERR_STORAGE = "RUM_STORAGE_ERROR"
458
+ private const val ERR_DEVICE_INFO = "RUM_DEVICE_INFO_ERROR"
459
+ private const val ERR_CRASH = "RUM_CRASH_ERROR"
460
+
461
+ init {
462
+ try {
463
+ System.loadLibrary("rum_crash")
464
+ } catch (_: UnsatisfiedLinkError) {
465
+ // Library may already be loaded by NdkCrashHandler
466
+ }
467
+ }
468
+
469
+ /**
470
+ * 迁移旧版 SharedPreferences(com.aliyun.arms.rum.reactnative → com.aliyun.rum.reactnative)
471
+ */
472
+ private fun migratePrefs(context: android.content.Context): SharedPreferences {
473
+ val newPrefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
474
+ val legacyPrefs = context.getSharedPreferences(LEGACY_PREFS_NAME, Context.MODE_PRIVATE)
475
+ val legacyData = legacyPrefs.all
476
+ if (legacyData.isNotEmpty()) {
477
+ val editor = newPrefs.edit()
478
+ for ((key, value) in legacyData) {
479
+ if (!newPrefs.contains(key)) {
480
+ when (value) {
481
+ is String -> editor.putString(key, value)
482
+ is Int -> editor.putInt(key, value)
483
+ is Long -> editor.putLong(key, value)
484
+ is Boolean -> editor.putBoolean(key, value)
485
+ is Float -> editor.putFloat(key, value)
486
+ is Set<*> -> {
487
+ @Suppress("UNCHECKED_CAST")
488
+ editor.putStringSet(key, value as Set<String>)
489
+ }
490
+ }
491
+ }
492
+ }
493
+ editor.apply()
494
+ legacyPrefs.edit().clear().apply()
495
+ }
496
+ return newPrefs
497
+ }
498
+
499
+ /**
500
+ * 判断文件名是否为崩溃报告文件(排除 context 文件和临时文件)。
501
+ * 崩溃文件:crash_*.json(Java)或 crash_native_*.json(NDK)
502
+ * 非崩溃:.rum_crash_context、crash_context.json(旧版)、.arms_crash_context(旧版)、*.tmp
503
+ */
504
+ private fun isCrashReportFile(name: String): Boolean {
505
+ if (!name.endsWith(".json")) return false
506
+ if (!name.startsWith("crash_")) return false
507
+ // 排除旧版 context 文件(升级后可能残留)
508
+ if (name == "crash_context.json") return false
509
+ return true
510
+ }
511
+ }
512
+ }
@@ -0,0 +1,33 @@
1
+ package com.rumsdkreactnative
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import java.util.HashMap
9
+
10
+ class RumSdkReactnativePackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
+ return if (name == RumSdkReactnativeModule.NAME) {
13
+ RumSdkReactnativeModule(reactContext)
14
+ } else {
15
+ null
16
+ }
17
+ }
18
+
19
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
20
+ return ReactModuleInfoProvider {
21
+ val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
22
+ moduleInfos[RumSdkReactnativeModule.NAME] = ReactModuleInfo(
23
+ RumSdkReactnativeModule.NAME,
24
+ RumSdkReactnativeModule.NAME,
25
+ false, // canOverrideExistingModule
26
+ false, // needsEagerInit
27
+ false, // isCxxModule
28
+ true // isTurboModule
29
+ )
30
+ moduleInfos
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,157 @@
1
+ package com.rumsdkreactnative.crash
2
+
3
+ import android.content.Context
4
+ import org.json.JSONObject
5
+ import java.io.BufferedReader
6
+ import java.io.File
7
+ import java.io.FileOutputStream
8
+ import java.io.InputStreamReader
9
+
10
+ /**
11
+ * 崩溃上下文管理器:
12
+ * - context 文件:单条 JSON,包含 session_id / view_id / view_name 等。原子写入(写临时文件 → rename)
13
+ *
14
+ * 注意:所有写入都基于普通文件 IO(非 SharedPreferences),保证 native crash 时也能被 fork 子进程读取。
15
+ */
16
+ class CrashContextManager(context: Context) {
17
+
18
+ private val baseDir: File = File(context.filesDir, CRASH_DIR_NAME).apply {
19
+ // 迁移旧版目录 arms_crashes → rum_crashes
20
+ val legacyDir = File(context.filesDir, LEGACY_CRASH_DIR_NAME)
21
+ if (legacyDir.exists() && legacyDir.isDirectory) {
22
+ try {
23
+ if (!exists()) {
24
+ legacyDir.renameTo(this)
25
+ } else {
26
+ legacyDir.listFiles()?.forEach { file ->
27
+ val target = File(this, file.name)
28
+ if (!target.exists()) file.renameTo(target)
29
+ }
30
+ legacyDir.delete()
31
+ }
32
+ } catch (_: Throwable) {
33
+ // ignore
34
+ }
35
+ }
36
+ if (!exists()) {
37
+ try {
38
+ mkdirs()
39
+ } catch (_: Throwable) {
40
+ // ignore
41
+ }
42
+ }
43
+ }
44
+
45
+ private val contextFile: File = File(baseDir, CONTEXT_FILE)
46
+ private val contextTmpFile: File = File(baseDir, "$CONTEXT_FILE.tmp")
47
+
48
+ init {
49
+ // 迁移清理:删除旧版 crash_context.json,迁移 .arms_crash_context → .rum_crash_context
50
+ migrateLegacyContextFile()
51
+ }
52
+
53
+ private fun migrateLegacyContextFile() {
54
+ try {
55
+ // 删除最早版本的 context 文件(json 后缀,会被崩溃过滤器误匹配)
56
+ val legacyJson = File(baseDir, LEGACY_CONTEXT_FILE)
57
+ if (legacyJson.exists()) {
58
+ legacyJson.delete()
59
+ }
60
+ // 迁移 .arms_crash_context → .rum_crash_context
61
+ val legacyArms = File(baseDir, LEGACY_ARMS_CONTEXT_FILE)
62
+ if (legacyArms.exists()) {
63
+ if (!contextFile.exists()) {
64
+ legacyArms.renameTo(contextFile)
65
+ } else {
66
+ legacyArms.delete()
67
+ }
68
+ }
69
+ } catch (_: Throwable) {
70
+ // ignore
71
+ }
72
+ }
73
+
74
+ // ===== Context =====
75
+
76
+ /** 原子写入上下文(写临时文件 → rename) */
77
+ @Synchronized
78
+ fun updateContext(jsonString: String) {
79
+ if (jsonString.isEmpty()) return
80
+ ensureDir()
81
+ var fos: FileOutputStream? = null
82
+ try {
83
+ fos = FileOutputStream(contextTmpFile)
84
+ fos.write(jsonString.toByteArray(Charsets.UTF_8))
85
+ fos.flush()
86
+ try {
87
+ fos.fd.sync()
88
+ } catch (_: Throwable) {
89
+ // ignore
90
+ }
91
+ try {
92
+ fos.close()
93
+ } catch (_: Throwable) {
94
+ // ignore
95
+ }
96
+ fos = null
97
+ // POSIX rename 保证原子替换目标文件,无需先 delete
98
+ try {
99
+ contextTmpFile.renameTo(contextFile)
100
+ } catch (_: Throwable) {
101
+ // ignore
102
+ }
103
+ } catch (_: Throwable) {
104
+ // ignore
105
+ } finally {
106
+ try {
107
+ fos?.close()
108
+ } catch (_: Throwable) {
109
+ // ignore
110
+ }
111
+ }
112
+ }
113
+
114
+ fun readContext(): JSONObject? {
115
+ if (!contextFile.exists()) return null
116
+ return try {
117
+ val text = readAllText(contextFile)
118
+ if (text.isEmpty()) null else JSONObject(text)
119
+ } catch (_: Throwable) {
120
+ null
121
+ }
122
+ }
123
+
124
+ // ===== Helpers =====
125
+
126
+ private fun ensureDir() {
127
+ if (!baseDir.exists()) {
128
+ try {
129
+ baseDir.mkdirs()
130
+ } catch (_: Throwable) {
131
+ // ignore
132
+ }
133
+ }
134
+ }
135
+
136
+ private fun readAllText(file: File): String {
137
+ return try {
138
+ file.inputStream().use { input ->
139
+ BufferedReader(InputStreamReader(input, Charsets.UTF_8)).use { reader ->
140
+ reader.readText()
141
+ }
142
+ }
143
+ } catch (_: Throwable) {
144
+ ""
145
+ }
146
+ }
147
+
148
+ companion object {
149
+ private const val CRASH_DIR_NAME = "rum_crashes"
150
+ private const val LEGACY_CRASH_DIR_NAME = "arms_crashes"
151
+ // 使用 .rum_ 前缀 + 非 .json 后缀,避免被崩溃文件过滤器误匹配
152
+ private const val CONTEXT_FILE = ".rum_crash_context"
153
+ // 旧版文件名(用于升级时迁移清理)
154
+ private const val LEGACY_CONTEXT_FILE = "crash_context.json"
155
+ private const val LEGACY_ARMS_CONTEXT_FILE = ".arms_crash_context"
156
+ }
157
+ }