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() { fun startCrashCapture() {
if (crashCaptureStarted) return if (crashCaptureStarted) return
if (!XuqmSDK.isInitialized()) return if (!XuqmSDK.isInitialized()) return
val logApiUrl = XuqmSDK.bugCollectApiUrl
?: (XuqmSDK.platformUrl.trimEnd('/') + "/api/bugcollect/")
crashCaptureStarted = true crashCaptureStarted = true
CrashCapture.start( CrashCapture.start(
appContext = XuqmSDK.appContext, appContext = XuqmSDK.appContext,
logApiUrl = logApiUrl,
appKey = XuqmSDK.appKey, appKey = XuqmSDK.appKey,
getUserId = { XuqmSDK.getUserId() }, getUserId = { XuqmSDK.getUserId() },
) )

查看文件

@ -4,11 +4,16 @@ import android.content.Context
import com.xuqm.sdk.bugcollect.internal.LogStorage import com.xuqm.sdk.bugcollect.internal.LogStorage
internal object CrashCapture { 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() val prev = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
// 使用带 Context 的重载,依赖 context.filesDir 获取可靠路径 LogStorage.saveCrash(appContext, throwable, appKey, getUserId())
LogStorage.saveCrash(appContext, throwable, logApiUrl, appKey, getUserId())
prev?.uncaughtException(thread, throwable) prev?.uncaughtException(thread, throwable)
} }
} }

查看文件

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

查看文件

@ -17,15 +17,14 @@ import kotlinx.coroutines.launch
/** /**
* 崩溃收集的早期初始化入口 * 崩溃收集的早期初始化入口
* *
* initOrder=89 sdk-core XuqmInitializerProvider(90) 之后运行此时 * initOrder=89 sdk-core XuqmInitializerProvider(90) 之后运行此时
* - XuqmSDK.appKey / platformUrl 已由本地 config 文件同步设置 * XuqmSDK.appKey 已由本地 config 文件同步设置
* - 远端平台配置尚未到达bugCollectApiUrl 可能还是 null
* *
* 职责 * 职责分离
* 1. 立即注册 UncaughtExceptionHandler把崩溃落盘只需 appKey + 派生 URL * Phase 1同步只注册 UncaughtExceptionHandler把崩溃落盘
* 2. 后台等待 XuqmSDK.awaitInitialization() 完成后上传积压的崩溃文件 * 不需要上传 URL注册与 URL 完全解耦
* * Phase 2异步等待 XuqmSDK.awaitInitialization() 完成
* sdk-core 不依赖本类XuqmSDK 可独立使用 * 远端配置下发 bugCollectApiUrl 后再上传积压的崩溃文件
*/ */
internal class BugCollectInitProvider : ContentProvider() { internal class BugCollectInitProvider : ContentProvider() {
@ -33,54 +32,28 @@ internal class BugCollectInitProvider : ContentProvider() {
val ctx = context?.applicationContext ?: return true val ctx = context?.applicationContext ?: return true
if (!XuqmSDK.isInitialized()) { if (!XuqmSDK.isInitialized()) {
// core 未初始化config 文件缺失等),跳过 Log.w("BugCollect", "BugCollectInitProvider: XuqmSDK not initialized, skipping")
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)")
return true return true
} }
// ── Phase 1立即注册拦截器,只需 appKey,无需 URL ──────────────────────
CrashCapture.start( CrashCapture.start(
appContext = ctx, appContext = ctx,
logApiUrl = logApiUrl,
appKey = XuqmSDK.appKey, appKey = XuqmSDK.appKey,
getUserId = { XuqmSDK.getUserId() }, getUserId = { XuqmSDK.getUserId() },
) )
// ── Phase 2: 等远端配置就绪后上传积压的崩溃文件 ───────────────────────── // ── 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) {
CoroutineScope(SupervisorJob() + Dispatchers.IO).launch { CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
runCatching { runCatching {
XuqmSDK.awaitInitialization() XuqmSDK.awaitInitialization()
// 远端配置到达后 bugCollectApiUrl 已更新,用最新 URL 上传
BugCollect.uploadPendingCrashes() BugCollect.uploadPendingCrashes()
}.onFailure { e -> }.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 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 中调用 * 同步写崩溃文件 UncaughtExceptionHandler 中调用
* Application Context handler 回调时仍然有效直接用 context.filesDir 获取可靠路径 * 不存储上传 URLURL 在下次启动上传时从 XuqmSDK.bugCollectApiUrl 取得
* 文件在下次启动时由 LogQueue.uploadPendingCrashes 上报后删除
*/ */
fun saveCrash( fun saveCrash(
context: Context, context: Context,
throwable: Throwable, throwable: Throwable,
logApiUrl: String,
appKey: String, appKey: String,
userId: String?, userId: String?,
) { ) {
@ -31,7 +29,6 @@ internal object LogStorage {
put("message", throwable.message ?: throwable.javaClass.name) put("message", throwable.message ?: throwable.javaClass.name)
put("stack", throwable.stackTraceToString()) put("stack", throwable.stackTraceToString())
put("exceptionType", throwable.javaClass.simpleName) put("exceptionType", throwable.javaClass.simpleName)
put("logApiUrl", logApiUrl)
put("appKey", appKey) put("appKey", appKey)
put("userId", userId ?: JSONObject.NULL) put("userId", userId ?: JSONObject.NULL)
put("timestamp", System.currentTimeMillis()) put("timestamp", System.currentTimeMillis())