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>
这个提交包含在:
父节点
be8eeba9e1
当前提交
77fef3a38b
@ -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 获取可靠路径。
|
* 不存储上传 URL——URL 在下次启动上传时从 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())
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户