refactor(bugcollect): 崩溃收集与上传分离,sdk-core 保持独立
设计问题:
- 之前用反射在 XuqmInitializerProvider 里调用 BugCollect,sdk-core 隐式依赖 sdk-bugcollect
- startCrashCapture 同时负责注册拦截器和触发上传,两者生命周期不同
新架构:
- sdk-core 完全干净,XuqmInitializerProvider 还原为只调用 autoInitialize
- sdk-bugcollect 自注册 BugCollectInitProvider(initOrder=89)
- Phase 1(同步):core init 完成后立即注册 UncaughtExceptionHandler,用
platformUrl 派生兜底 URL,落盘不依赖远端配置
- Phase 2(异步):awaitInitialization() 后用最新 bugCollectApiUrl 上传积压崩溃
- BugCollect.uploadPendingCrashes() 公开,供 Phase 2 调用
- startCrashCapture() 保持幂等,app 侧可选择性调用,不影响自动流程
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
cc1557daf0
当前提交
54b3163f53
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<!--
|
||||
initOrder=89 < sdk-core XuqmInitializerProvider(90),确保 core 先完成本地初始化,
|
||||
BugCollect 能读到 XuqmSDK.appKey / platformUrl 后立即注册 UncaughtExceptionHandler。
|
||||
-->
|
||||
<provider
|
||||
android:name="com.xuqm.sdk.bugcollect.internal.BugCollectInitProvider"
|
||||
android:authorities="${applicationId}.xuqm-bugcollect-init"
|
||||
android:exported="false"
|
||||
android:initOrder="89" />
|
||||
</application>
|
||||
</manifest>
|
||||
@ -129,11 +129,14 @@ object BugCollect {
|
||||
event("__log_info", tags + ("message" to message))
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册全局崩溃拦截器(幂等)。
|
||||
* 通常由 BugCollectInitProvider 在 app 启动时自动调用,无需 app 代码主动触发。
|
||||
* 如需在特定时机手动注册,调用此方法也是安全的。
|
||||
*/
|
||||
fun startCrashCapture() {
|
||||
if (crashCaptureStarted) return
|
||||
if (!XuqmSDK.isInitialized()) return
|
||||
// bugCollectApiUrl 由远端配置下发,启动时可能尚未到达。
|
||||
// 用 platformUrl 推导兜底地址:崩溃文件里会保存此 URL,下次启动时用它上报。
|
||||
val logApiUrl = XuqmSDK.bugCollectApiUrl
|
||||
?: (XuqmSDK.platformUrl.trimEnd('/') + "/api/bugcollect/")
|
||||
crashCaptureStarted = true
|
||||
@ -146,6 +149,11 @@ object BugCollect {
|
||||
queue().uploadPendingCrashes()
|
||||
}
|
||||
|
||||
/** 上传上次崩溃落盘的文件(远端配置就绪后由 BugCollectInitProvider 自动调用)。 */
|
||||
fun uploadPendingCrashes() {
|
||||
if (isReady()) queue().uploadPendingCrashes()
|
||||
}
|
||||
|
||||
fun defineFunnel(id: String, steps: List<String>) {
|
||||
FunnelTracker.define(id, steps)
|
||||
}
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
package com.xuqm.sdk.bugcollect.internal
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.xuqm.sdk.XuqmSDK
|
||||
import com.xuqm.sdk.bugcollect.BugCollect
|
||||
import com.xuqm.sdk.bugcollect.CrashCapture
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 崩溃收集的早期初始化入口。
|
||||
*
|
||||
* initOrder=89,在 sdk-core XuqmInitializerProvider(90) 之后运行,此时:
|
||||
* - XuqmSDK.appKey / platformUrl 已由本地 config 文件同步设置
|
||||
* - 远端平台配置尚未到达(bugCollectApiUrl 可能还是 null)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 立即注册 UncaughtExceptionHandler,把崩溃落盘(只需 appKey + 派生 URL)
|
||||
* 2. 后台等待 XuqmSDK.awaitInitialization() 完成后,上传积压的崩溃文件
|
||||
*
|
||||
* sdk-core 不依赖本类,XuqmSDK 可独立使用。
|
||||
*/
|
||||
internal class BugCollectInitProvider : ContentProvider() {
|
||||
|
||||
override fun onCreate(): Boolean {
|
||||
val ctx = context?.applicationContext ?: return true
|
||||
|
||||
if (!XuqmSDK.isInitialized()) {
|
||||
// core 未初始化(config 文件缺失等),跳过
|
||||
Log.w("BugCollect", "BugCollectInitProvider: XuqmSDK not initialized, skipping crash capture setup")
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Phase 1: 立即注册崩溃拦截,落盘不需要远端配置 ──────────────────────
|
||||
val logApiUrl = XuqmSDK.bugCollectApiUrl
|
||||
?: (XuqmSDK.platformUrl.trimEnd('/') + "/api/bugcollect/")
|
||||
|
||||
CrashCapture.start(
|
||||
appContext = ctx,
|
||||
logApiUrl = logApiUrl,
|
||||
appKey = XuqmSDK.appKey,
|
||||
getUserId = { XuqmSDK.getUserId() },
|
||||
)
|
||||
|
||||
// ── Phase 2: 等远端配置就绪后上传积压的崩溃文件 ─────────────────────────
|
||||
uploadAfterInit(ctx)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun uploadAfterInit(ctx: Context) {
|
||||
CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
|
||||
runCatching {
|
||||
XuqmSDK.awaitInitialization()
|
||||
// 远端配置到达后 bugCollectApiUrl 已更新,用最新 URL 上传
|
||||
BugCollect.uploadPendingCrashes()
|
||||
}.onFailure { e ->
|
||||
Log.w("BugCollect", "Failed to upload pending crashes after init: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun query(uri: Uri, p: Array<out String>?, s: String?, sa: Array<out String>?, so: String?): Cursor? = null
|
||||
override fun getType(uri: Uri): String? = null
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
|
||||
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int = 0
|
||||
}
|
||||
@ -19,26 +19,12 @@ class XuqmInitializerProvider : ContentProvider() {
|
||||
if (!XuqmSDK.isInitialized()) {
|
||||
XuqmSDK.autoInitialize(ctx)
|
||||
}
|
||||
// 在 SDK 初始化后立即启动崩溃捕获,确保任何后续崩溃都能被落盘。
|
||||
// startCrashCapture 内部使用 platformUrl 兜底,无需等远端配置到达。
|
||||
startCrashCaptureIfAvailable()
|
||||
}.onFailure { e ->
|
||||
Log.w("XuqmSDK", "Auto-initialization skipped: ${e.message}")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun startCrashCaptureIfAvailable() {
|
||||
runCatching {
|
||||
val bugCollect = Class.forName("com.xuqm.sdk.bugcollect.BugCollect")
|
||||
val companion = bugCollect.getField("INSTANCE").get(null)
|
||||
val method = bugCollect.getMethod("startCrashCapture")
|
||||
method.invoke(companion)
|
||||
}.onFailure {
|
||||
// sdk-bugcollect 未依赖时跳过,不影响其他功能
|
||||
}
|
||||
}
|
||||
|
||||
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
|
||||
override fun getType(uri: Uri): String? = null
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户