refactor(bugcollect): 注册与 URL 完全解耦

注册 UncaughtExceptionHandler 不需要 URL:
- CrashCapture.start 移除 logApiUrl 参数,只需 appContext + appKey
- LogStorage.saveCrash 移除 logApiUrl,崩溃文件不再存储 URL
- LogQueue.uploadPendingCrashes 上传时直接取 XuqmSDK.bugCollectApiUrl,
  URL 为 null 则跳过(下次启动重试),不依赖崩溃文件里的历史 URL
- BugCollect.startCrashCapture 移除 URL 构造逻辑
- BugCollectInitProvider Phase 1 纯粹只注册,彻底消除对 platformUrl 的依赖,
  版本兼容问题从根本上消失

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-06-18 10:18:08 +08:00
父节点 be8eeba9e1
当前提交 77fef3a38b
共有 5 个文件被更改,包括 24 次插入53 次删除

查看文件

@ -137,12 +137,9 @@ object BugCollect {
fun startCrashCapture() {
if (crashCaptureStarted) return
if (!XuqmSDK.isInitialized()) return
val logApiUrl = XuqmSDK.bugCollectApiUrl
?: (XuqmSDK.platformUrl.trimEnd('/') + "/api/bugcollect/")
crashCaptureStarted = true
CrashCapture.start(
appContext = XuqmSDK.appContext,
logApiUrl = logApiUrl,
appKey = XuqmSDK.appKey,
getUserId = { XuqmSDK.getUserId() },
)

查看文件

@ -4,11 +4,16 @@ import android.content.Context
import com.xuqm.sdk.bugcollect.internal.LogStorage
internal object CrashCapture {
fun start(appContext: Context, logApiUrl: String, appKey: String, getUserId: () -> String?) {
@Volatile private var started = false
/** 注册全局崩溃拦截器,只需 appContext + appKey,不依赖上传 URL。 */
fun start(appContext: Context, appKey: String, getUserId: () -> String?) {
if (started) return
started = true
val prev = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
// 使用带 Context 的重载,依赖 context.filesDir 获取可靠路径
LogStorage.saveCrash(appContext, throwable, logApiUrl, appKey, getUserId())
LogStorage.saveCrash(appContext, throwable, appKey, getUserId())
prev?.uncaughtException(thread, throwable)
}
}

查看文件

@ -65,7 +65,8 @@ internal class LogQueue(
}
fun uploadPendingCrashes() {
if (logApiUrl.isBlank()) return
// 上传时才需要 URL,从远端配置取最新值
val uploadUrl = com.xuqm.sdk.XuqmSDK.bugCollectApiUrl ?: return
scope.launch {
val crashDir = LogStorage.crashDir(appContext)
val files = crashDir.listFiles() ?: return@launch
@ -94,8 +95,6 @@ internal class LogQueue(
userId = obj.optNullableString("userId"),
user = IssueEvent.UserInfo(id = obj.optNullableString("userId")),
)
// 优先使用崩溃文件里保存的 URL崩溃时可能远端配置未到达
val uploadUrl = obj.optString("logApiUrl").takeIf { it.isNotBlank() } ?: logApiUrl
LogUploader.uploadIssue(uploadUrl, event)
file.delete()
}.onFailure { e ->

查看文件

@ -17,15 +17,14 @@ import kotlinx.coroutines.launch
/**
* 崩溃收集的早期初始化入口
*
* initOrder=89 sdk-core XuqmInitializerProvider(90) 之后运行此时
* - XuqmSDK.appKey / platformUrl 已由本地 config 文件同步设置
* - 远端平台配置尚未到达bugCollectApiUrl 可能还是 null
* initOrder=89 sdk-core XuqmInitializerProvider(90) 之后运行此时
* XuqmSDK.appKey 已由本地 config 文件同步设置
*
* 职责
* 1. 立即注册 UncaughtExceptionHandler把崩溃落盘只需 appKey + 派生 URL
* 2. 后台等待 XuqmSDK.awaitInitialization() 完成后上传积压的崩溃文件
*
* sdk-core 不依赖本类XuqmSDK 可独立使用
* 职责分离
* Phase 1同步只注册 UncaughtExceptionHandler把崩溃落盘
* 不需要上传 URL注册与 URL 完全解耦
* Phase 2异步等待 XuqmSDK.awaitInitialization() 完成
* 远端配置下发 bugCollectApiUrl 后再上传积压的崩溃文件
*/
internal class BugCollectInitProvider : ContentProvider() {
@ -33,54 +32,28 @@ internal class BugCollectInitProvider : ContentProvider() {
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
?: deriveFallbackUrl()
if (logApiUrl.isBlank()) {
Log.w("BugCollect", "Crash capture skipped: no URL available (update sdk-core to latest SNAPSHOT)")
Log.w("BugCollect", "BugCollectInitProvider: XuqmSDK not initialized, skipping")
return true
}
// ── Phase 1立即注册拦截器,只需 appKey,无需 URL ──────────────────────
CrashCapture.start(
appContext = ctx,
logApiUrl = logApiUrl,
appKey = XuqmSDK.appKey,
getUserId = { XuqmSDK.getUserId() },
)
// ── Phase 2: 等远端配置就绪后上传积压的崩溃文件 ─────────────────────────
uploadAfterInit(ctx)
return true
}
/**
* XuqmSDK.platformUrl 派生兜底地址
* try-catch 保护旧版 sdk-core 没有此属性时返回空串让调用方跳过
*/
private fun deriveFallbackUrl(): String = runCatching {
XuqmSDK.platformUrl.trimEnd('/') + "/api/bugcollect/"
}.getOrElse { e ->
Log.w("BugCollect", "Cannot derive bugcollect URL (sdk-core may be outdated): ${e.message}")
""
}
private fun uploadAfterInit(ctx: Context) {
// ── Phase 2远端配置就绪后上传 ─────────────────────────────────────────
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}")
Log.w("BugCollect", "Pending crash upload failed after init: ${e.message}")
}
}
return true
}
override fun query(uri: Uri, p: Array<out String>?, s: String?, sa: Array<out String>?, so: String?): Cursor? = null

查看文件

@ -13,13 +13,11 @@ internal object LogStorage {
/**
* 同步写崩溃文件 UncaughtExceptionHandler 中调用
* Application Context handler 回调时仍然有效直接用 context.filesDir 获取可靠路径
* 文件在下次启动时由 LogQueue.uploadPendingCrashes 上报后删除
* 不存储上传 URLURL 在下次启动上传时从 XuqmSDK.bugCollectApiUrl 取得
*/
fun saveCrash(
context: Context,
throwable: Throwable,
logApiUrl: String,
appKey: String,
userId: String?,
) {
@ -31,7 +29,6 @@ internal object LogStorage {
put("message", throwable.message ?: throwable.javaClass.name)
put("stack", throwable.stackTraceToString())
put("exceptionType", throwable.javaClass.simpleName)
put("logApiUrl", logApiUrl)
put("appKey", appKey)
put("userId", userId ?: JSONObject.NULL)
put("timestamp", System.currentTimeMillis())