feat(bugcollect): SDK v1.1.0 — 修复 packageName 缺失导致 captureError 静默失败

Root cause: SdkPlatformConfigApi.fetchConfig 未传 packageName → 服务端返回400
→ bugCollectEnabled=false → captureError 是空操作

修复内容:
- SdkPlatformConfigApi: 增加 packageName 查询参数
- SdkPlatformConfig: 修正字段名 bugCollectApiUrl/features.bugCollect
- XuqmSDK: 传入 appContext.packageName,读取 features?.bugCollect
- Fingerprint: 使用 exceptionType(类名)替代 level 字符串,避免同一崩溃按级别分桶
- IssueEvent: 增加 eventId、breadcrumbs、DeviceInfo 扩展字段
- BugCollect: 增加 addBreadcrumb(),captureError/captureCrash 附加面包屑和设备信息
- LogUploader: 序列化 eventId/breadcrumbs/expanded device
- LogQueue: 崩溃恢复使用 exceptionType 重建 fingerprint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-06-17 15:30:40 +08:00
父节点 a36097fcdb
当前提交 4896f24af8
共有 10 个文件被更改,包括 311 次插入79 次删除

查看文件

@ -0,0 +1,9 @@
package com.xuqm.sdk.bugcollect
data class Breadcrumb(
val timestamp: Long = System.currentTimeMillis(),
val category: String,
val message: String,
val level: String = "info",
val data: Map<String, Any?> = emptyMap(),
)

查看文件

@ -1,6 +1,11 @@
package com.xuqm.sdk.bugcollect package com.xuqm.sdk.bugcollect
import android.app.ActivityManager
import android.content.Context
import android.content.pm.ApplicationInfo
import android.os.Build
import com.xuqm.sdk.XuqmSDK import com.xuqm.sdk.XuqmSDK
import java.util.LinkedList
object BugCollect { object BugCollect {
@ -9,40 +14,122 @@ object BugCollect {
@Volatile private var queue: LogQueue? = null @Volatile private var queue: LogQueue? = null
@Volatile private var crashCaptureStarted = false @Volatile private var crashCaptureStarted = false
private val breadcrumbBuffer: LinkedList<Breadcrumb> = LinkedList()
private val breadcrumbLock = Any()
private const val MAX_BREADCRUMBS = 50
private const val SDK_NAME = "bugcollect.android"
private const val SDK_VERSION = "1.1.0"
fun setLogLevel(level: LogLevel) { logLevel = level } fun setLogLevel(level: LogLevel) { logLevel = level }
fun setEnvironment(env: String) { environment = env } fun setEnvironment(env: String) { environment = env }
fun addBreadcrumb(
category: String,
message: String,
level: String = "info",
data: Map<String, Any?> = emptyMap(),
) {
synchronized(breadcrumbLock) {
if (breadcrumbBuffer.size >= MAX_BREADCRUMBS) breadcrumbBuffer.removeFirst()
breadcrumbBuffer.addLast(Breadcrumb(
timestamp = System.currentTimeMillis(),
category = category,
message = message,
level = level,
data = data,
))
}
}
fun event(name: String, properties: Map<String, Any?> = emptyMap()) { fun event(name: String, properties: Map<String, Any?> = emptyMap()) {
if (!isReady()) return if (!isReady()) return
queue().push(LogEvent( queue().push(LogEvent(
name = name, properties = properties, name = name, properties = properties,
appKey = XuqmSDK.appKey, userId = XuqmSDK.getUserId(), appKey = XuqmSDK.appKey, userId = XuqmSDK.getUserId(),
platform = "android", appVersion = appVersion(), environment = environment, platform = "android", release = appVersion(), environment = environment,
sdk = IssueEvent.SdkInfo(SDK_NAME, SDK_VERSION),
)) ))
FunnelTracker.track(name, properties) FunnelTracker.track(name, properties)
} }
fun captureError(error: Throwable, metadata: Map<String, Any?> = emptyMap()) { fun captureError(error: Throwable, tags: Map<String, Any?> = emptyMap(), fingerprint: String? = null) {
if (!isReady()) return if (!isReady()) return
val crumbs = synchronized(breadcrumbLock) { breadcrumbBuffer.toList() }
val fp = fingerprint ?: Fingerprint.compute(
error.javaClass.simpleName,
error.message ?: error.javaClass.name,
error.stackTraceToString(),
)
queue().push(IssueEvent( queue().push(IssueEvent(
type = "android_error", level = "error",
message = error.message ?: error.javaClass.name, platform = "android",
stack = error.stackTraceToString(), fingerprint = fp,
fingerprint = Fingerprint.compute("error", error.message ?: "", error.stackTraceToString()), appKey = XuqmSDK.appKey,
appKey = XuqmSDK.appKey, userId = XuqmSDK.getUserId(), exception = IssueEvent.ExceptionInfo(
platform = "android", appVersion = appVersion(), type = error.javaClass.simpleName,
metadata = metadata, environment = environment, value = error.message ?: error.javaClass.name,
stacktrace = error.stackTraceToString(),
),
breadcrumbs = crumbs,
release = appVersion(),
environment = environment,
userId = XuqmSDK.getUserId(),
user = IssueEvent.UserInfo(id = XuqmSDK.getUserId()),
device = buildDeviceInfo(),
tags = tags,
sdk = IssueEvent.SdkInfo(SDK_NAME, SDK_VERSION),
)) ))
} }
fun warn(message: String, metadata: Map<String, Any?> = emptyMap()) { fun captureCrash(error: Throwable, tags: Map<String, Any?> = emptyMap()) {
if (logLevel.ordinal > LogLevel.WARN.ordinal) return if (!isReady()) return
captureError(Exception(message), metadata + ("level" to "warn")) val crumbs = synchronized(breadcrumbLock) { breadcrumbBuffer.toList() }
queue().push(IssueEvent(
level = "fatal",
platform = "android",
fingerprint = Fingerprint.compute(
error.javaClass.simpleName,
error.message ?: error.javaClass.name,
error.stackTraceToString(),
),
appKey = XuqmSDK.appKey,
exception = IssueEvent.ExceptionInfo(
type = error.javaClass.simpleName,
value = error.message ?: error.javaClass.name,
stacktrace = error.stackTraceToString(),
),
breadcrumbs = crumbs,
release = appVersion(),
environment = environment,
userId = XuqmSDK.getUserId(),
user = IssueEvent.UserInfo(id = XuqmSDK.getUserId()),
device = buildDeviceInfo(),
tags = tags,
sdk = IssueEvent.SdkInfo(SDK_NAME, SDK_VERSION),
))
} }
fun info(message: String, metadata: Map<String, Any?> = emptyMap()) { fun warn(message: String, tags: Map<String, Any?> = emptyMap()) {
if (logLevel.ordinal > LogLevel.WARN.ordinal) return
if (!isReady()) return
queue().push(IssueEvent(
level = "warning",
platform = "android",
fingerprint = Fingerprint.compute("Warning", message, ""),
appKey = XuqmSDK.appKey,
exception = IssueEvent.ExceptionInfo(type = "Warning", value = message),
release = appVersion(),
environment = environment,
userId = XuqmSDK.getUserId(),
user = IssueEvent.UserInfo(id = XuqmSDK.getUserId()),
tags = tags,
sdk = IssueEvent.SdkInfo(SDK_NAME, SDK_VERSION),
))
}
fun info(message: String, tags: Map<String, Any?> = emptyMap()) {
if (logLevel.ordinal > LogLevel.INFO.ordinal) return if (logLevel.ordinal > LogLevel.INFO.ordinal) return
event("__log_info", metadata + ("message" to message)) event("__log_info", tags + ("message" to message))
} }
fun startCrashCapture() { fun startCrashCapture() {
@ -77,4 +164,28 @@ object BugCollect {
val ctx = XuqmSDK.appContext val ctx = XuqmSDK.appContext
ctx.packageManager.getPackageInfo(ctx.packageName, 0).versionName ?: "unknown" ctx.packageManager.getPackageInfo(ctx.packageName, 0).versionName ?: "unknown"
}.getOrDefault("unknown") }.getOrDefault("unknown")
private fun buildDeviceInfo(): IssueEvent.DeviceInfo {
val ctx = XuqmSDK.appContext
val actMgr = ctx.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
val memInfo = ActivityManager.MemoryInfo().also { actMgr?.getMemoryInfo(it) }
val freeMemMb = (memInfo.availMem / (1024L * 1024L)).toInt()
val isDebug = ctx.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
val isEmulator = Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.startsWith("unknown")
|| Build.MODEL.contains("Emulator", ignoreCase = true)
|| Build.MODEL.contains("Android SDK", ignoreCase = true)
return IssueEvent.DeviceInfo(
model = Build.MODEL,
manufacturer = Build.MANUFACTURER,
osName = "Android",
osVersion = Build.VERSION.RELEASE,
locale = java.util.Locale.getDefault().toString(),
timezone = java.util.TimeZone.getDefault().id,
isEmulator = isEmulator,
freeMemoryMb = freeMemMb,
buildType = if (isDebug) "debug" else "release",
)
}
} }

查看文件

@ -1,10 +1,11 @@
package com.xuqm.sdk.bugcollect package com.xuqm.sdk.bugcollect
object Fingerprint { object Fingerprint {
fun compute(type: String, message: String, stack: String): String { // exceptionType = exception class name (e.g. "NullPointerException"), NOT the level string
fun compute(exceptionType: String, message: String, stack: String): String {
val top3 = stack.lines().filter { it.trim().startsWith("at ") }.take(3).joinToString("|") val top3 = stack.lines().filter { it.trim().startsWith("at ") }.take(3).joinToString("|")
val normalized = message.replace(Regex("\\b\\d{4,}\\b"), "N") val normalized = message.replace(Regex("\\b\\d{4,}\\b"), "N")
return sha256("$type:$normalized:$top3") return sha256("$exceptionType:$normalized:$top3")
} }
private fun sha256(s: String): String = private fun sha256(s: String): String =

查看文件

@ -1,15 +1,43 @@
package com.xuqm.sdk.bugcollect package com.xuqm.sdk.bugcollect
data class IssueEvent( data class IssueEvent(
val type: String, val eventId: String = java.util.UUID.randomUUID().toString(),
val message: String, val level: String,
val stack: String, val platform: String,
val fingerprint: String, val fingerprint: String,
val appKey: String, val appKey: String,
val userId: String?,
val platform: String,
val appVersion: String,
val metadata: Map<String, Any?> = emptyMap(),
val environment: String,
val timestamp: Long = System.currentTimeMillis(), val timestamp: Long = System.currentTimeMillis(),
) val exception: ExceptionInfo? = null,
val breadcrumbs: List<Breadcrumb> = emptyList(),
val release: String,
val environment: String = "production",
val userId: String? = null,
val sessionId: String? = null,
val user: UserInfo? = null,
val device: DeviceInfo? = null,
val tags: Map<String, Any?> = emptyMap(),
) {
data class ExceptionInfo(
val type: String,
val value: String,
val stacktrace: String? = null,
)
data class UserInfo(
val id: String? = null,
)
data class DeviceInfo(
val name: String? = null,
val model: String? = null,
val manufacturer: String? = null,
val osName: String? = null,
val osVersion: String? = null,
val locale: String? = null,
val timezone: String? = null,
val network: String? = null,
val isEmulator: Boolean? = null,
val freeMemoryMb: Int? = null,
val buildType: String? = null,
)
}

查看文件

@ -1,12 +1,17 @@
package com.xuqm.sdk.bugcollect package com.xuqm.sdk.bugcollect
data class LogEvent( data class LogEvent(
val eventId: String? = java.util.UUID.randomUUID().toString(),
val name: String, val name: String,
val properties: Map<String, Any?> = emptyMap(), val properties: Map<String, Any?> = emptyMap(),
val appKey: String, val appKey: String,
val userId: String?, val userId: String? = null,
val sessionId: String? = null,
val platform: String, val platform: String,
val appVersion: String, val release: String,
val environment: String, val environment: String = "production",
val timestamp: Long = System.currentTimeMillis(), val timestamp: Long = System.currentTimeMillis(),
val user: IssueEvent.UserInfo? = null,
val device: IssueEvent.DeviceInfo? = null,
val sdk: IssueEvent.SdkInfo? = null,
) )

查看文件

@ -73,20 +73,27 @@ internal class LogQueue(
runCatching { runCatching {
val json = file.readText() val json = file.readText()
val obj = JSONObject(json) val obj = JSONObject(json)
val msg = obj.optString("message", "")
val stk = obj.optString("stack", "")
val exType = obj.optString("exceptionType", "Exception")
val event = IssueEvent( val event = IssueEvent(
type = obj.optString("type", "native_crash"), level = "fatal",
message = obj.optString("message", ""),
stack = obj.optString("stack", ""),
fingerprint = Fingerprint.compute("native_crash", obj.optString("message", ""), obj.optString("stack", "")),
appKey = obj.optString("appKey", appKey),
userId = obj.optNullableString("userId"),
platform = "android", platform = "android",
appVersion = runCatching { fingerprint = Fingerprint.compute(exType, msg, stk),
appKey = obj.optString("appKey", appKey),
timestamp = obj.optLong("timestamp", System.currentTimeMillis()),
exception = IssueEvent.ExceptionInfo(
type = exType,
value = msg,
stacktrace = stk,
),
release = runCatching {
appContext.packageManager.getPackageInfo(appContext.packageName, 0).versionName ?: "unknown" appContext.packageManager.getPackageInfo(appContext.packageName, 0).versionName ?: "unknown"
}.getOrDefault("unknown"), }.getOrDefault("unknown"),
metadata = emptyMap(),
environment = "production", environment = "production",
timestamp = obj.optLong("timestamp", System.currentTimeMillis()), userId = obj.optNullableString("userId"),
user = IssueEvent.UserInfo(id = obj.optNullableString("userId")),
sdk = IssueEvent.SdkInfo("bugcollect.android", "1.0.0"),
) )
LogUploader.uploadIssue(logApiUrl, event) LogUploader.uploadIssue(logApiUrl, event)
file.delete() file.delete()
@ -146,17 +153,14 @@ internal class LogQueue(
for (i in 0 until arr.length()) { for (i in 0 until arr.length()) {
val obj = arr.getJSONObject(i) val obj = arr.getJSONObject(i)
pendingIssues.add(IssueEvent( pendingIssues.add(IssueEvent(
type = obj.optString("type", ""), level = obj.optString("level", "error"),
message = obj.optString("message", ""), platform = obj.optString("platform", "android"),
stack = obj.optString("stack", ""),
fingerprint = obj.optString("fingerprint", ""), fingerprint = obj.optString("fingerprint", ""),
appKey = obj.optString("appKey", appKey), appKey = obj.optString("appKey", appKey),
userId = obj.optNullableString("userId"),
platform = obj.optString("platform", "android"),
appVersion = obj.optString("appVersion", ""),
metadata = emptyMap(),
environment = obj.optString("environment", "production"),
timestamp = obj.optLong("timestamp", 0), timestamp = obj.optLong("timestamp", 0),
release = obj.optString("release", ""),
environment = obj.optString("environment", "production"),
userId = obj.optNullableString("userId"),
)) ))
} }
} }
@ -171,7 +175,7 @@ internal class LogQueue(
appKey = obj.optString("appKey", appKey), appKey = obj.optString("appKey", appKey),
userId = obj.optNullableString("userId"), userId = obj.optNullableString("userId"),
platform = obj.optString("platform", "android"), platform = obj.optString("platform", "android"),
appVersion = obj.optString("appVersion", ""), release = obj.optString("release", ""),
environment = obj.optString("environment", "production"), environment = obj.optString("environment", "production"),
timestamp = obj.optLong("timestamp", 0), timestamp = obj.optLong("timestamp", 0),
)) ))

查看文件

@ -29,9 +29,10 @@ internal object LogStorage {
).also { it.mkdirs() } ).also { it.mkdirs() }
val file = File(dir, "${System.currentTimeMillis()}.json") val file = File(dir, "${System.currentTimeMillis()}.json")
val json = JSONObject().apply { val json = JSONObject().apply {
put("type", "native_crash") put("level", "fatal")
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("logApiUrl", logApiUrl) put("logApiUrl", logApiUrl)
put("appKey", appKey) put("appKey", appKey)
put("userId", userId ?: JSONObject.NULL) put("userId", userId ?: JSONObject.NULL)
@ -55,9 +56,10 @@ internal object LogStorage {
val dir = crashDir(context) val dir = crashDir(context)
val file = File(dir, "${System.currentTimeMillis()}.json") val file = File(dir, "${System.currentTimeMillis()}.json")
val json = JSONObject().apply { val json = JSONObject().apply {
put("type", "native_crash") put("level", "fatal")
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("logApiUrl", logApiUrl) put("logApiUrl", logApiUrl)
put("appKey", appKey) put("appKey", appKey)
put("userId", userId ?: JSONObject.NULL) put("userId", userId ?: JSONObject.NULL)

查看文件

@ -9,6 +9,9 @@ import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
internal object LogUploader { internal object LogUploader {
@ -25,28 +28,44 @@ internal object LogUploader {
fun uploadIssues(logApiUrl: String, issues: List<IssueEvent>) { fun uploadIssues(logApiUrl: String, issues: List<IssueEvent>) {
if (issues.isEmpty()) return if (issues.isEmpty()) return
val url = "${logApiUrl.trimEnd('/')}/log/v1/issues/batch" val url = "${logApiUrl.trimEnd('/')}/bugcollect/v1/issues/batch"
val array = JSONArray() val envelope = createEnvelope()
val events = JSONArray()
for (issue in issues) { for (issue in issues) {
array.put(issueToJson(issue)) events.put(issueToJson(issue))
} }
post(url, array.toString()) envelope.put("events", events)
post(url, envelope.toString())
} }
fun uploadEvents(logApiUrl: String, events: List<LogEvent>) { fun uploadEvents(logApiUrl: String, events: List<LogEvent>) {
if (events.isEmpty()) return if (events.isEmpty()) return
val url = "${logApiUrl.trimEnd('/')}/log/v1/events/batch" val url = "${logApiUrl.trimEnd('/')}/bugcollect/v1/events/batch"
val array = JSONArray() val envelope = createEnvelope()
val eventsArray = JSONArray()
for (event in events) { for (event in events) {
array.put(eventToJson(event)) eventsArray.put(eventToJson(event))
} }
post(url, array.toString()) envelope.put("events", eventsArray)
post(url, envelope.toString())
} }
fun uploadIssue(logApiUrl: String, issue: IssueEvent) { fun uploadIssue(logApiUrl: String, issue: IssueEvent) {
val url = "${logApiUrl.trimEnd('/')}/log/v1/issues/batch" val url = "${logApiUrl.trimEnd('/')}/bugcollect/v1/issues/batch"
val array = JSONArray().put(issueToJson(issue)) val envelope = createEnvelope()
post(url, array.toString()) val events = JSONArray().put(issueToJson(issue))
envelope.put("events", events)
post(url, envelope.toString())
}
private fun createEnvelope(): JSONObject = JSONObject().apply {
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
sdf.timeZone = TimeZone.getTimeZone("UTC")
put("sentAt", sdf.format(System.currentTimeMillis()))
put("sdk", JSONObject().apply {
put("name", "bugcollect.android")
put("version", "1.0.0")
})
} }
private fun post(url: String, body: String) { private fun post(url: String, body: String) {
@ -62,27 +81,71 @@ internal object LogUploader {
} }
private fun issueToJson(issue: IssueEvent): JSONObject = JSONObject().apply { private fun issueToJson(issue: IssueEvent): JSONObject = JSONObject().apply {
put("type", issue.type) put("eventId", issue.eventId)
put("message", issue.message)
put("stack", issue.stack)
put("fingerprint", issue.fingerprint)
put("appKey", issue.appKey) put("appKey", issue.appKey)
put("userId", issue.userId ?: JSONObject.NULL) put("level", issue.level)
put("platform", issue.platform) put("platform", issue.platform)
put("appVersion", issue.appVersion) put("fingerprint", issue.fingerprint)
put("environment", issue.environment)
put("timestamp", issue.timestamp) put("timestamp", issue.timestamp)
put("metadata", JSONObject(issue.metadata as? Map<String, Any?> ?: emptyMap<String, Any?>())) put("release", issue.release)
put("environment", issue.environment)
if (issue.exception != null) {
put("exception", JSONObject().apply {
put("type", issue.exception.type)
put("value", issue.exception.value)
if (issue.exception.stacktrace != null) put("stacktrace", issue.exception.stacktrace)
})
}
if (issue.breadcrumbs.isNotEmpty()) {
val arr = JSONArray()
for (crumb in issue.breadcrumbs) {
arr.put(JSONObject().apply {
put("timestamp", crumb.timestamp)
put("category", crumb.category)
put("message", crumb.message)
put("level", crumb.level)
if (crumb.data.isNotEmpty()) {
put("data", JSONObject(crumb.data as? Map<String, Any?> ?: emptyMap<String, Any?>()))
}
})
}
put("breadcrumbs", arr)
}
if (issue.userId != null || issue.user?.id != null) {
put("user", JSONObject().apply { put("id", issue.userId ?: issue.user?.id) })
}
if (issue.device != null) {
put("device", JSONObject().apply {
issue.device.model?.let { put("model", it) }
issue.device.manufacturer?.let { put("manufacturer", it) }
issue.device.osName?.let { put("osName", it) }
issue.device.osVersion?.let { put("osVersion", it) }
issue.device.locale?.let { put("locale", it) }
issue.device.timezone?.let { put("timezone", it) }
issue.device.network?.let { put("network", it) }
issue.device.isEmulator?.let { put("isEmulator", it) }
issue.device.freeMemoryMb?.let { put("freeMemoryMb", it) }
issue.device.buildType?.let { put("buildType", it) }
})
}
if (issue.tags.isNotEmpty()) {
put("tags", JSONObject(issue.tags as? Map<String, Any?> ?: emptyMap<String, Any?>()))
}
} }
private fun eventToJson(event: LogEvent): JSONObject = JSONObject().apply { private fun eventToJson(event: LogEvent): JSONObject = JSONObject().apply {
put("name", event.name) event.eventId?.let { put("eventId", it) }
put("properties", JSONObject(event.properties as? Map<String, Any?> ?: emptyMap<String, Any?>()))
put("appKey", event.appKey) put("appKey", event.appKey)
put("userId", event.userId ?: JSONObject.NULL) put("name", event.name)
put("platform", event.platform)
put("appVersion", event.appVersion)
put("environment", event.environment)
put("timestamp", event.timestamp) put("timestamp", event.timestamp)
put("platform", event.platform)
put("release", event.release)
put("environment", event.environment)
if (event.properties.isNotEmpty()) {
put("properties", JSONObject(event.properties as? Map<String, Any?> ?: emptyMap<String, Any?>()))
}
if (event.userId != null || event.user?.id != null) {
put("user", JSONObject().apply { put("id", event.userId ?: event.user?.id) })
}
} }
} }

查看文件

@ -182,7 +182,7 @@ object XuqmSDK {
private suspend fun fetchAndApplyPlatformConfig(platformUrl: String, appKey: String) { private suspend fun fetchAndApplyPlatformConfig(platformUrl: String, appKey: String) {
val base = platformUrl.trimEnd('/') + "/" val base = platformUrl.trimEnd('/') + "/"
val api = ApiClient.create(SdkPlatformConfigApi::class.java, base) val api = ApiClient.create(SdkPlatformConfigApi::class.java, base)
val response = api.fetchConfig(appKey) val response = api.fetchConfig(appKey, packageName = appContext.packageName)
val cfg = response.data val cfg = response.data
?: throw IllegalStateException( ?: throw IllegalStateException(
"Platform returned empty config for appKey=$appKey at $platformUrl. " + "Platform returned empty config for appKey=$appKey at $platformUrl. " +
@ -206,7 +206,7 @@ object XuqmSDK {
) )
) )
bugCollectApiUrl = cfg.bugCollectApiUrl bugCollectApiUrl = cfg.bugCollectApiUrl
bugCollectEnabled = cfg.bugCollectEnabled ?: false bugCollectEnabled = cfg.features?.bugCollect ?: false
Log.i( Log.i(
TAG, TAG,
"Platform config applied [${if (platformUrl == DEFAULT_PLATFORM_URL) "public" else "private"}]:" + "Platform config applied [${if (platformUrl == DEFAULT_PLATFORM_URL) "public" else "private"}]:" +

查看文件

@ -9,6 +9,7 @@ internal interface SdkPlatformConfigApi {
suspend fun fetchConfig( suspend fun fetchConfig(
@Query("appKey") appKey: String, @Query("appKey") appKey: String,
@Query("platform") platform: String = "ANDROID", @Query("platform") platform: String = "ANDROID",
@Query("packageName") packageName: String = "",
): SdkPlatformConfigResponse ): SdkPlatformConfigResponse
} }
@ -27,8 +28,16 @@ data class SdkPlatformConfig(
val imWsUrl: String? = null, val imWsUrl: String? = null,
/** 文件服务地址 */ /** 文件服务地址 */
val fileServiceUrl: String? = null, val fileServiceUrl: String? = null,
/** Bug 收集上报服务地址(旧服务端不返回时为 null */ /** Bug 收集上报服务地址,由平台下发 */
@SerializedName("logApiUrl") val bugCollectApiUrl: String? = null, @SerializedName("bugCollectApiUrl") val bugCollectApiUrl: String? = null,
/** 是否启用客户端 Bug 收集上报(旧服务端不返回时为 null,视为 false */ /** 各服务开通状态,由平台下发 */
@SerializedName("logEnabled") val bugCollectEnabled: Boolean? = null, val features: SdkPlatformFeatures? = null,
)
/** 平台各服务开通状态。 */
data class SdkPlatformFeatures(
val bugCollect: Boolean? = null,
val im: Boolean? = null,
val push: Boolean? = null,
val update: Boolean? = null,
) )