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
|
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,
|
||||||
)
|
)
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户