@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.
- package/README.md +79 -1
- package/RumSdkReactnative.podspec +28 -0
- package/android/build.gradle +83 -0
- package/android/gradle.properties +6 -0
- package/android/src/main/AndroidManifest.xml +13 -0
- package/android/src/main/java/com/rumsdkreactnative/RumSdkReactnativeModule.kt +512 -0
- package/android/src/main/java/com/rumsdkreactnative/RumSdkReactnativePackage.kt +33 -0
- package/android/src/main/java/com/rumsdkreactnative/crash/CrashContextManager.kt +157 -0
- package/android/src/main/java/com/rumsdkreactnative/crash/CrashHandler.kt +444 -0
- package/android/src/main/java/com/rumsdkreactnative/crash/CrashInitProvider.kt +50 -0
- package/android/src/main/java/com/rumsdkreactnative/crash/NdkCrashHandler.kt +66 -0
- package/android/src/main/jni/CMakeLists.txt +21 -0
- package/android/src/main/jni/rum_backtrace.c +144 -0
- package/android/src/main/jni/rum_crash_handler.c +200 -0
- package/android/src/main/jni/rum_crash_writer.c +442 -0
- package/android/src/main/jni/rum_module_info.c +316 -0
- package/android/src/main/jni/rum_module_info.h +54 -0
- package/android/src/main/jni/rum_test_crash.cpp +55 -0
- package/app.plugin.js +11 -0
- package/docs/ReactNative SDK/351/205/215/347/275/256/345/217/202/350/200/203.md" +671 -0
- package/docs/SDK /347/211/210/346/234/254/350/257/264/346/230/216.md" +12 -0
- package/docs//346/216/245/345/205/245ReactNative/345/272/224/347/224/250.md +238 -0
- package/ios/PrivacyInfo.xcprivacy +93 -0
- package/ios/RumCrashReporter.h +38 -0
- package/ios/RumCrashReporter.mm +861 -0
- package/ios/RumSdkReactnative.h +5 -0
- package/ios/RumSdkReactnative.mm +291 -0
- package/ios/fishhook/fishhook.c +264 -0
- package/ios/fishhook/fishhook.h +76 -0
- package/lib/commonjs/NativeRumSdkReactnative.js +50 -0
- package/lib/commonjs/collector/action/index.js +108 -0
- package/lib/commonjs/collector/exception/crash.js +148 -0
- package/lib/commonjs/collector/exception/index.js +238 -0
- package/lib/commonjs/collector/index.js +42 -0
- package/lib/commonjs/collector/resource/api.js +286 -0
- package/lib/commonjs/collector/view/perf.js +79 -0
- package/lib/commonjs/collector/view/pv.js +88 -0
- package/lib/commonjs/expoConfigPlugin.js +37 -0
- package/lib/commonjs/index.js +29 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/plugin.js +47 -0
- package/lib/commonjs/processor/default-processor.js +27 -0
- package/lib/commonjs/processor/session-processor.js +47 -0
- package/lib/commonjs/reporter/index.js +148 -0
- package/lib/commonjs/reporter/monitor-reporter.js +43 -0
- package/lib/commonjs/shell.js +91 -0
- package/lib/commonjs/types/client.js +6 -0
- package/lib/commonjs/types/crash.js +37 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/commonjs/utils/action.js +305 -0
- package/lib/commonjs/utils/api.js +83 -0
- package/lib/commonjs/utils/base.js +95 -0
- package/lib/commonjs/utils/crash.js +146 -0
- package/lib/commonjs/utils/platform.js +162 -0
- package/lib/commonjs/utils/session.js +158 -0
- package/lib/commonjs/utils/url.js +59 -0
- package/lib/commonjs/utils/view.js +93 -0
- package/lib/module/NativeRumSdkReactnative.js +47 -0
- package/lib/module/collector/action/index.js +45 -10
- package/lib/module/collector/exception/crash.js +143 -0
- package/lib/module/collector/exception/index.js +144 -46
- package/lib/module/collector/index.js +8 -0
- package/lib/module/collector/resource/api.js +6 -6
- package/lib/module/collector/view/pv.js +11 -0
- package/lib/module/expoConfigPlugin.js +33 -0
- package/lib/module/index.js +1 -0
- package/lib/module/processor/session-processor.js +22 -0
- package/lib/module/reporter/index.js +102 -16
- package/lib/module/reporter/monitor-reporter.js +38 -0
- package/lib/module/shell.js +11 -7
- package/lib/module/types/crash.js +33 -0
- package/lib/module/utils/action.js +299 -0
- package/lib/module/utils/crash.js +141 -0
- package/lib/module/utils/platform.js +124 -72
- package/lib/module/utils/session.js +23 -4
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/NativeRumSdkReactnative.d.ts +49 -0
- package/lib/typescript/{src → commonjs/src}/collector/action/index.d.ts +6 -0
- package/lib/typescript/commonjs/src/collector/exception/crash.d.ts +28 -0
- package/lib/typescript/{src → commonjs/src}/collector/exception/index.d.ts +7 -0
- package/lib/typescript/commonjs/src/collector/index.d.ts +6 -0
- package/lib/typescript/commonjs/src/expoConfigPlugin.d.ts +28 -0
- package/lib/typescript/commonjs/src/index.d.ts +6 -0
- package/lib/typescript/{src → commonjs/src}/processor/session-processor.d.ts +2 -0
- package/lib/typescript/{src → commonjs/src}/reporter/index.d.ts +3 -1
- package/lib/typescript/commonjs/src/reporter/monitor-reporter.d.ts +8 -0
- package/lib/typescript/{src → commonjs/src}/shell.d.ts +11 -0
- package/lib/typescript/commonjs/src/types/crash.d.ts +95 -0
- package/lib/typescript/commonjs/src/utils/action.d.ts +63 -0
- package/lib/typescript/commonjs/src/utils/crash.d.ts +12 -0
- package/lib/typescript/commonjs/src/utils/platform.d.ts +22 -0
- package/lib/typescript/{src → commonjs/src}/utils/session.d.ts +4 -0
- package/lib/typescript/module/src/NativeRumSdkReactnative.d.ts +49 -0
- package/lib/typescript/module/src/collector/action/index.d.ts +21 -0
- package/lib/typescript/module/src/collector/exception/crash.d.ts +28 -0
- package/lib/typescript/module/src/collector/exception/index.d.ts +32 -0
- package/lib/typescript/module/src/collector/index.d.ts +6 -0
- package/lib/typescript/module/src/collector/resource/api.d.ts +21 -0
- package/lib/typescript/module/src/collector/view/perf.d.ts +1 -0
- package/lib/typescript/module/src/collector/view/pv.d.ts +16 -0
- package/lib/typescript/module/src/expoConfigPlugin.d.ts +28 -0
- package/lib/typescript/module/src/index.d.ts +6 -0
- package/lib/typescript/module/src/plugin.d.ts +3 -0
- package/lib/typescript/module/src/processor/default-processor.d.ts +6 -0
- package/lib/typescript/module/src/processor/session-processor.d.ts +11 -0
- package/lib/typescript/module/src/reporter/index.d.ts +18 -0
- package/lib/typescript/module/src/reporter/monitor-reporter.d.ts +8 -0
- package/lib/typescript/module/src/shell.d.ts +57 -0
- package/lib/typescript/module/src/types/client.d.ts +27 -0
- package/lib/typescript/module/src/types/crash.d.ts +95 -0
- package/lib/typescript/module/src/types/index.d.ts +118 -0
- package/lib/typescript/module/src/utils/action.d.ts +63 -0
- package/lib/typescript/module/src/utils/api.d.ts +59 -0
- package/lib/typescript/module/src/utils/base.d.ts +8 -0
- package/lib/typescript/module/src/utils/crash.d.ts +12 -0
- package/lib/typescript/module/src/utils/platform.d.ts +22 -0
- package/lib/typescript/module/src/utils/session.d.ts +41 -0
- package/lib/typescript/module/src/utils/url.d.ts +17 -0
- package/lib/typescript/module/src/utils/view.d.ts +13 -0
- package/package.json +51 -28
- package/lib/module/collector/action/index.js.map +0 -1
- package/lib/module/collector/exception/index.js.map +0 -1
- package/lib/module/collector/resource/api.js.map +0 -1
- package/lib/module/collector/view/perf.js.map +0 -1
- package/lib/module/collector/view/pv.js.map +0 -1
- package/lib/module/index.js.map +0 -1
- package/lib/module/plugin.js.map +0 -1
- package/lib/module/processor/default-processor.js.map +0 -1
- package/lib/module/processor/session-processor.js.map +0 -1
- package/lib/module/reporter/index.js.map +0 -1
- package/lib/module/shell.js.map +0 -1
- package/lib/module/types/client.js.map +0 -1
- package/lib/module/types/index.js.map +0 -1
- package/lib/module/utils/api.js.map +0 -1
- package/lib/module/utils/base.js.map +0 -1
- package/lib/module/utils/platform.js.map +0 -1
- package/lib/module/utils/session.js.map +0 -1
- package/lib/module/utils/url.js.map +0 -1
- package/lib/module/utils/view.js.map +0 -1
- package/lib/typescript/src/collector/action/index.d.ts.map +0 -1
- package/lib/typescript/src/collector/exception/index.d.ts.map +0 -1
- package/lib/typescript/src/collector/resource/api.d.ts.map +0 -1
- package/lib/typescript/src/collector/view/perf.d.ts.map +0 -1
- package/lib/typescript/src/collector/view/pv.d.ts.map +0 -1
- package/lib/typescript/src/index.d.ts +0 -4
- package/lib/typescript/src/index.d.ts.map +0 -1
- package/lib/typescript/src/plugin.d.ts.map +0 -1
- package/lib/typescript/src/processor/default-processor.d.ts.map +0 -1
- package/lib/typescript/src/processor/session-processor.d.ts.map +0 -1
- package/lib/typescript/src/reporter/index.d.ts.map +0 -1
- package/lib/typescript/src/shell.d.ts.map +0 -1
- package/lib/typescript/src/types/client.d.ts.map +0 -1
- package/lib/typescript/src/types/index.d.ts.map +0 -1
- package/lib/typescript/src/utils/api.d.ts.map +0 -1
- package/lib/typescript/src/utils/base.d.ts.map +0 -1
- package/lib/typescript/src/utils/platform.d.ts +0 -15
- package/lib/typescript/src/utils/platform.d.ts.map +0 -1
- package/lib/typescript/src/utils/session.d.ts.map +0 -1
- package/lib/typescript/src/utils/url.d.ts.map +0 -1
- package/lib/typescript/src/utils/view.d.ts.map +0 -1
- /package/lib/typescript/{src → commonjs/src}/collector/resource/api.d.ts +0 -0
- /package/lib/typescript/{src → commonjs/src}/collector/view/perf.d.ts +0 -0
- /package/lib/typescript/{src → commonjs/src}/collector/view/pv.d.ts +0 -0
- /package/lib/typescript/{src → commonjs/src}/plugin.d.ts +0 -0
- /package/lib/typescript/{src → commonjs/src}/processor/default-processor.d.ts +0 -0
- /package/lib/typescript/{src → commonjs/src}/types/client.d.ts +0 -0
- /package/lib/typescript/{src → commonjs/src}/types/index.d.ts +0 -0
- /package/lib/typescript/{src → commonjs/src}/utils/api.d.ts +0 -0
- /package/lib/typescript/{src → commonjs/src}/utils/base.d.ts +0 -0
- /package/lib/typescript/{src → commonjs/src}/utils/url.d.ts +0 -0
- /package/lib/typescript/{src → commonjs/src}/utils/view.d.ts +0 -0
- /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
|
+
}
|