- LogQueue.logApiUrl 改为动态 getter,每次上传取最新 URL - flush() 在 URL 为空时跳过,等下次定时 flush - LogUploader.resolveUrl 自动补全相对路径
218 行
8.2 KiB
Kotlin
218 行
8.2 KiB
Kotlin
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 {
|
|
|
|
private var logLevel: LogLevel = LogLevel.WARN
|
|
private var environment: String = "production"
|
|
@Volatile private var queue: LogQueue? = null
|
|
@Volatile private var crashCaptureStarted = false
|
|
|
|
private val breadcrumbBuffer: LinkedList<Breadcrumb> = LinkedList()
|
|
private val breadcrumbLock = Any()
|
|
// SDK 未就绪时暂存的错误事件,就绪后自动 flush
|
|
private val pendingErrors = LinkedList<IssueEvent>()
|
|
private val pendingErrorsLock = 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() ?: XuqmSDK.deviceId,
|
|
platform = "android", release = appVersion(), environment = environment,
|
|
sdk = IssueEvent.SdkInfo(SDK_NAME, SDK_VERSION),
|
|
))
|
|
FunnelTracker.track(name, properties)
|
|
}
|
|
|
|
fun captureError(error: Throwable, tags: Map<String, Any?> = emptyMap(), fingerprint: String? = null) {
|
|
val crumbs = synchronized(breadcrumbLock) { breadcrumbBuffer.toList() }
|
|
val fp = fingerprint ?: Fingerprint.compute(
|
|
error.javaClass.simpleName,
|
|
error.message ?: error.javaClass.name,
|
|
error.stackTraceToString(),
|
|
)
|
|
val event = IssueEvent(
|
|
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() ?: XuqmSDK.deviceId,
|
|
user = IssueEvent.UserInfo(id = XuqmSDK.getUserId() ?: XuqmSDK.deviceId),
|
|
device = buildDeviceInfo(),
|
|
tags = tags,
|
|
)
|
|
if (!isReady()) {
|
|
synchronized(pendingErrorsLock) { pendingErrors.add(event) }
|
|
return
|
|
}
|
|
flushPendingErrors()
|
|
queue().push(event)
|
|
}
|
|
|
|
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() ?: XuqmSDK.deviceId,
|
|
user = IssueEvent.UserInfo(id = XuqmSDK.getUserId() ?: XuqmSDK.deviceId),
|
|
device = buildDeviceInfo(),
|
|
tags = tags,
|
|
))
|
|
}
|
|
|
|
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() ?: XuqmSDK.deviceId,
|
|
user = IssueEvent.UserInfo(id = XuqmSDK.getUserId() ?: XuqmSDK.deviceId),
|
|
tags = tags,
|
|
))
|
|
}
|
|
|
|
fun info(message: String, tags: Map<String, Any?> = emptyMap()) {
|
|
if (logLevel.ordinal > LogLevel.INFO.ordinal) return
|
|
event("__log_info", tags + ("message" to message))
|
|
}
|
|
|
|
/**
|
|
* 注册全局崩溃拦截器(幂等)。
|
|
* 通常由 BugCollectInitProvider 在 app 启动时自动调用,无需 app 代码主动触发。
|
|
* 如需在特定时机手动注册,调用此方法也是安全的。
|
|
*/
|
|
fun startCrashCapture() {
|
|
if (crashCaptureStarted) return
|
|
if (!XuqmSDK.isInitialized()) return
|
|
crashCaptureStarted = true
|
|
CrashCapture.start(
|
|
appContext = XuqmSDK.appContext,
|
|
appKey = XuqmSDK.appKey,
|
|
getUserId = { XuqmSDK.getUserId() ?: XuqmSDK.deviceId },
|
|
)
|
|
flushPendingErrors()
|
|
queue().uploadPendingCrashes()
|
|
}
|
|
|
|
/** 将 SDK 未就绪时暂存的错误事件 flush 到队列。 */
|
|
private fun flushPendingErrors() {
|
|
val events = synchronized(pendingErrorsLock) {
|
|
if (pendingErrors.isEmpty()) return
|
|
val list = pendingErrors.toList()
|
|
pendingErrors.clear()
|
|
list
|
|
}
|
|
events.forEach { queue().push(it) }
|
|
}
|
|
|
|
/** 上传上次崩溃落盘的文件(远端配置就绪后由 BugCollectInitProvider 自动调用)。 */
|
|
fun uploadPendingCrashes() {
|
|
if (isReady()) queue().uploadPendingCrashes()
|
|
}
|
|
|
|
fun defineFunnel(id: String, steps: List<String>) {
|
|
FunnelTracker.define(id, steps)
|
|
}
|
|
|
|
private fun isReady() = XuqmSDK.isInitialized() && XuqmSDK.bugCollectEnabled
|
|
|
|
private fun queue(): LogQueue {
|
|
return queue ?: synchronized(this) {
|
|
queue ?: LogQueue(
|
|
appKey = XuqmSDK.appKey,
|
|
appContext = XuqmSDK.appContext,
|
|
).also { queue = it }
|
|
}
|
|
}
|
|
|
|
private fun appVersion(): String = runCatching {
|
|
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",
|
|
)
|
|
}
|
|
}
|