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>
这个提交包含在:
父节点
a36097fcdb
当前提交
4896f24af8
@ -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
|
||||
|
||||
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<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 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()) {
|
||||
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<String, Any?> = emptyMap()) {
|
||||
fun captureError(error: Throwable, tags: Map<String, Any?> = 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<String, Any?> = emptyMap()) {
|
||||
if (logLevel.ordinal > LogLevel.WARN.ordinal) return
|
||||
captureError(Exception(message), metadata + ("level" to "warn"))
|
||||
fun captureCrash(error: Throwable, tags: Map<String, Any?> = 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<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
|
||||
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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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<String, Any?> = emptyMap(),
|
||||
val environment: String,
|
||||
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
|
||||
|
||||
data class LogEvent(
|
||||
val eventId: String? = java.util.UUID.randomUUID().toString(),
|
||||
val name: String,
|
||||
val properties: Map<String, Any?> = 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,
|
||||
)
|
||||
|
||||
@ -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),
|
||||
))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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<IssueEvent>) {
|
||||
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<LogEvent>) {
|
||||
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<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 {
|
||||
put("name", event.name)
|
||||
put("properties", JSONObject(event.properties as? Map<String, Any?> ?: emptyMap<String, Any?>()))
|
||||
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<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) {
|
||||
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"}]:" +
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户