From 4896f24af87a1a95672c3bffc66802d6d29eeed1 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Wed, 17 Jun 2026 15:30:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(bugcollect):=20SDK=20v1.1.0=20=E2=80=94=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20packageName=20=E7=BC=BA=E5=A4=B1=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=20captureError=20=E9=9D=99=E9=BB=98=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../com/xuqm/sdk/bugcollect/Breadcrumb.kt | 9 ++ .../com/xuqm/sdk/bugcollect/BugCollect.kt | 139 ++++++++++++++++-- .../com/xuqm/sdk/bugcollect/Fingerprint.kt | 5 +- .../com/xuqm/sdk/bugcollect/IssueEvent.kt | 46 ++++-- .../java/com/xuqm/sdk/bugcollect/LogEvent.kt | 11 +- .../java/com/xuqm/sdk/bugcollect/LogQueue.kt | 40 ++--- .../sdk/bugcollect/internal/LogStorage.kt | 6 +- .../sdk/bugcollect/internal/LogUploader.kt | 113 ++++++++++---- .../src/main/java/com/xuqm/sdk/XuqmSDK.kt | 4 +- .../xuqm/sdk/network/SdkPlatformConfigApi.kt | 17 ++- 10 files changed, 311 insertions(+), 79 deletions(-) create mode 100644 sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/Breadcrumb.kt diff --git a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/Breadcrumb.kt b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/Breadcrumb.kt new file mode 100644 index 0000000..5bd069c --- /dev/null +++ b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/Breadcrumb.kt @@ -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 = emptyMap(), +) diff --git a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/BugCollect.kt b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/BugCollect.kt index 0721a73..8fd34f2 100644 --- a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/BugCollect.kt +++ b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/BugCollect.kt @@ -1,6 +1,11 @@ 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 java.util.LinkedList object BugCollect { @@ -9,40 +14,122 @@ object BugCollect { @Volatile private var queue: LogQueue? = null @Volatile private var crashCaptureStarted = false + private val breadcrumbBuffer: LinkedList = 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 setEnvironment(env: String) { environment = env } + fun addBreadcrumb( + category: String, + message: String, + level: String = "info", + data: Map = 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 = emptyMap()) { if (!isReady()) return queue().push(LogEvent( name = name, properties = properties, 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) } - fun captureError(error: Throwable, metadata: Map = emptyMap()) { + fun captureError(error: Throwable, tags: Map = emptyMap(), fingerprint: String? = null) { 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( - type = "android_error", - message = error.message ?: error.javaClass.name, - stack = error.stackTraceToString(), - fingerprint = Fingerprint.compute("error", error.message ?: "", error.stackTraceToString()), - appKey = XuqmSDK.appKey, userId = XuqmSDK.getUserId(), - platform = "android", appVersion = appVersion(), - metadata = metadata, environment = environment, + level = "error", + platform = "android", + fingerprint = fp, + 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 warn(message: String, metadata: Map = emptyMap()) { - if (logLevel.ordinal > LogLevel.WARN.ordinal) return - captureError(Exception(message), metadata + ("level" to "warn")) + fun captureCrash(error: Throwable, tags: Map = emptyMap()) { + if (!isReady()) return + 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 = emptyMap()) { + fun warn(message: String, tags: Map = 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 = emptyMap()) { if (logLevel.ordinal > LogLevel.INFO.ordinal) return - event("__log_info", metadata + ("message" to message)) + event("__log_info", tags + ("message" to message)) } fun startCrashCapture() { @@ -77,4 +164,28 @@ object BugCollect { val ctx = XuqmSDK.appContext ctx.packageManager.getPackageInfo(ctx.packageName, 0).versionName ?: "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", + ) + } } diff --git a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/Fingerprint.kt b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/Fingerprint.kt index a82268c..2e61475 100644 --- a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/Fingerprint.kt +++ b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/Fingerprint.kt @@ -1,10 +1,11 @@ package com.xuqm.sdk.bugcollect 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 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 = diff --git a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/IssueEvent.kt b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/IssueEvent.kt index ad540d1..b764f29 100644 --- a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/IssueEvent.kt +++ b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/IssueEvent.kt @@ -1,15 +1,43 @@ package com.xuqm.sdk.bugcollect data class IssueEvent( - val type: String, - val message: String, - val stack: String, + val eventId: String = java.util.UUID.randomUUID().toString(), + val level: String, + val platform: String, val fingerprint: String, val appKey: String, - val userId: String?, - val platform: String, - val appVersion: String, - val metadata: Map = emptyMap(), - val environment: String, val timestamp: Long = System.currentTimeMillis(), -) + val exception: ExceptionInfo? = null, + val breadcrumbs: List = 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 = 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, + ) +} diff --git a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/LogEvent.kt b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/LogEvent.kt index 139ff07..5e4dfc2 100644 --- a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/LogEvent.kt +++ b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/LogEvent.kt @@ -1,12 +1,17 @@ package com.xuqm.sdk.bugcollect data class LogEvent( + val eventId: String? = java.util.UUID.randomUUID().toString(), val name: String, val properties: Map = emptyMap(), val appKey: String, - val userId: String?, + val userId: String? = null, + val sessionId: String? = null, val platform: String, - val appVersion: String, - val environment: String, + val release: String, + val environment: String = "production", val timestamp: Long = System.currentTimeMillis(), + val user: IssueEvent.UserInfo? = null, + val device: IssueEvent.DeviceInfo? = null, + val sdk: IssueEvent.SdkInfo? = null, ) diff --git a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/LogQueue.kt b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/LogQueue.kt index 22f64a2..6095c3e 100644 --- a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/LogQueue.kt +++ b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/LogQueue.kt @@ -73,20 +73,27 @@ internal class LogQueue( runCatching { val json = file.readText() val obj = JSONObject(json) + val msg = obj.optString("message", "") + val stk = obj.optString("stack", "") + val exType = obj.optString("exceptionType", "Exception") val event = IssueEvent( - type = obj.optString("type", "native_crash"), - 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"), + level = "fatal", 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" }.getOrDefault("unknown"), - metadata = emptyMap(), 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) file.delete() @@ -146,17 +153,14 @@ internal class LogQueue( for (i in 0 until arr.length()) { val obj = arr.getJSONObject(i) pendingIssues.add(IssueEvent( - type = obj.optString("type", ""), - message = obj.optString("message", ""), - stack = obj.optString("stack", ""), + level = obj.optString("level", "error"), + platform = obj.optString("platform", "android"), fingerprint = obj.optString("fingerprint", ""), 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), + 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), userId = obj.optNullableString("userId"), platform = obj.optString("platform", "android"), - appVersion = obj.optString("appVersion", ""), + release = obj.optString("release", ""), environment = obj.optString("environment", "production"), timestamp = obj.optLong("timestamp", 0), )) diff --git a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/LogStorage.kt b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/LogStorage.kt index 724bd73..b019e43 100644 --- a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/LogStorage.kt +++ b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/LogStorage.kt @@ -29,9 +29,10 @@ internal object LogStorage { ).also { it.mkdirs() } val file = File(dir, "${System.currentTimeMillis()}.json") val json = JSONObject().apply { - put("type", "native_crash") + put("level", "fatal") 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) @@ -55,9 +56,10 @@ internal object LogStorage { val dir = crashDir(context) val file = File(dir, "${System.currentTimeMillis()}.json") val json = JSONObject().apply { - put("type", "native_crash") + put("level", "fatal") 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) diff --git a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/LogUploader.kt b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/LogUploader.kt index b799bfc..0aa94e5 100644 --- a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/LogUploader.kt +++ b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/internal/LogUploader.kt @@ -9,6 +9,9 @@ import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import org.json.JSONArray import org.json.JSONObject +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone import java.util.concurrent.TimeUnit internal object LogUploader { @@ -25,28 +28,44 @@ internal object LogUploader { fun uploadIssues(logApiUrl: String, issues: List) { if (issues.isEmpty()) return - val url = "${logApiUrl.trimEnd('/')}/log/v1/issues/batch" - val array = JSONArray() + val url = "${logApiUrl.trimEnd('/')}/bugcollect/v1/issues/batch" + val envelope = createEnvelope() + val events = JSONArray() 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) { if (events.isEmpty()) return - val url = "${logApiUrl.trimEnd('/')}/log/v1/events/batch" - val array = JSONArray() + val url = "${logApiUrl.trimEnd('/')}/bugcollect/v1/events/batch" + val envelope = createEnvelope() + val eventsArray = JSONArray() 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) { - val url = "${logApiUrl.trimEnd('/')}/log/v1/issues/batch" - val array = JSONArray().put(issueToJson(issue)) - post(url, array.toString()) + val url = "${logApiUrl.trimEnd('/')}/bugcollect/v1/issues/batch" + val envelope = createEnvelope() + 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) { @@ -62,27 +81,71 @@ internal object LogUploader { } private fun issueToJson(issue: IssueEvent): JSONObject = JSONObject().apply { - put("type", issue.type) - put("message", issue.message) - put("stack", issue.stack) - put("fingerprint", issue.fingerprint) + put("eventId", issue.eventId) put("appKey", issue.appKey) - put("userId", issue.userId ?: JSONObject.NULL) + put("level", issue.level) put("platform", issue.platform) - put("appVersion", issue.appVersion) - put("environment", issue.environment) + put("fingerprint", issue.fingerprint) put("timestamp", issue.timestamp) - put("metadata", JSONObject(issue.metadata as? Map ?: emptyMap())) + 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 ?: emptyMap())) + } + }) + } + 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 ?: emptyMap())) + } } private fun eventToJson(event: LogEvent): JSONObject = JSONObject().apply { - put("name", event.name) - put("properties", JSONObject(event.properties as? Map ?: emptyMap())) + event.eventId?.let { put("eventId", it) } put("appKey", event.appKey) - put("userId", event.userId ?: JSONObject.NULL) - put("platform", event.platform) - put("appVersion", event.appVersion) - put("environment", event.environment) + put("name", event.name) 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 ?: emptyMap())) + } + if (event.userId != null || event.user?.id != null) { + put("user", JSONObject().apply { put("id", event.userId ?: event.user?.id) }) + } } } diff --git a/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt b/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt index abde4f7..5f62659 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/XuqmSDK.kt @@ -182,7 +182,7 @@ object XuqmSDK { private suspend fun fetchAndApplyPlatformConfig(platformUrl: String, appKey: String) { val base = platformUrl.trimEnd('/') + "/" 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 ?: throw IllegalStateException( "Platform returned empty config for appKey=$appKey at $platformUrl. " + @@ -206,7 +206,7 @@ object XuqmSDK { ) ) bugCollectApiUrl = cfg.bugCollectApiUrl - bugCollectEnabled = cfg.bugCollectEnabled ?: false + bugCollectEnabled = cfg.features?.bugCollect ?: false Log.i( TAG, "Platform config applied [${if (platformUrl == DEFAULT_PLATFORM_URL) "public" else "private"}]:" + diff --git a/sdk-core/src/main/java/com/xuqm/sdk/network/SdkPlatformConfigApi.kt b/sdk-core/src/main/java/com/xuqm/sdk/network/SdkPlatformConfigApi.kt index f685da8..e6f08a7 100644 --- a/sdk-core/src/main/java/com/xuqm/sdk/network/SdkPlatformConfigApi.kt +++ b/sdk-core/src/main/java/com/xuqm/sdk/network/SdkPlatformConfigApi.kt @@ -9,6 +9,7 @@ internal interface SdkPlatformConfigApi { suspend fun fetchConfig( @Query("appKey") appKey: String, @Query("platform") platform: String = "ANDROID", + @Query("packageName") packageName: String = "", ): SdkPlatformConfigResponse } @@ -27,8 +28,16 @@ data class SdkPlatformConfig( val imWsUrl: String? = null, /** 文件服务地址 */ val fileServiceUrl: String? = null, - /** Bug 收集上报服务地址(旧服务端不返回时为 null) */ - @SerializedName("logApiUrl") val bugCollectApiUrl: String? = null, - /** 是否启用客户端 Bug 收集上报(旧服务端不返回时为 null,视为 false) */ - @SerializedName("logEnabled") val bugCollectEnabled: Boolean? = null, + /** Bug 收集上报服务地址,由平台下发 */ + @SerializedName("bugCollectApiUrl") val bugCollectApiUrl: String? = null, + /** 各服务开通状态,由平台下发 */ + val features: SdkPlatformFeatures? = null, +) + +/** 平台各服务开通状态。 */ +data class SdkPlatformFeatures( + val bugCollect: Boolean? = null, + val im: Boolean? = null, + val push: Boolean? = null, + val update: Boolean? = null, )