@arms/rum-reactnative 0.1.1 → 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 +399 -15
- 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 +13 -2
- package/lib/module/expoConfigPlugin.js +33 -0
- package/lib/module/index.js +1 -0
- package/lib/module/plugin.js +4 -1
- 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 +32 -7
- package/lib/module/types/crash.js +33 -0
- package/lib/module/utils/action.js +299 -0
- package/lib/module/utils/api.js +10 -10
- package/lib/module/utils/base.js +9 -9
- package/lib/module/utils/crash.js +141 -0
- package/lib/module/utils/platform.js +127 -72
- package/lib/module/utils/session.js +23 -4
- package/lib/module/utils/view.js +2 -1
- 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 +7 -1
- 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/{src → commonjs/src}/collector/view/pv.d.ts +2 -2
- 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/commonjs/src/shell.d.ts +57 -0
- package/lib/typescript/{src → commonjs/src}/types/client.d.ts +2 -2
- 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/{src → commonjs/src}/utils/api.d.ts +1 -1
- 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 +61 -26
- package/LICENSE +0 -20
- 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 +0 -26
- 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}/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/index.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,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
|
+
)
|