2026-06-16 17:39:23 +08:00
|
|
|
package com.xuqm.sdk.bugcollect
|
2026-06-16 12:10:58 +08:00
|
|
|
|
2026-06-17 15:30:40 +08:00
|
|
|
import android.app.ActivityManager
|
|
|
|
|
import android.content.Context
|
|
|
|
|
import android.content.pm.ApplicationInfo
|
|
|
|
|
import android.os.Build
|
2026-06-16 12:10:58 +08:00
|
|
|
import com.xuqm.sdk.XuqmSDK
|
2026-06-17 15:30:40 +08:00
|
|
|
import java.util.LinkedList
|
2026-06-16 12:10:58 +08:00
|
|
|
|
2026-06-16 17:39:23 +08:00
|
|
|
object BugCollect {
|
2026-06-16 12:10:58 +08:00
|
|
|
|
|
|
|
|
private var logLevel: LogLevel = LogLevel.WARN
|
|
|
|
|
private var environment: String = "production"
|
|
|
|
|
@Volatile private var queue: LogQueue? = null
|
|
|
|
|
@Volatile private var crashCaptureStarted = false
|
|
|
|
|
|
2026-06-17 15:30:40 +08:00
|
|
|
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"
|
|
|
|
|
|
2026-06-16 12:10:58 +08:00
|
|
|
fun setLogLevel(level: LogLevel) { logLevel = level }
|
|
|
|
|
fun setEnvironment(env: String) { environment = env }
|
|
|
|
|
|
2026-06-17 15:30:40 +08:00
|
|
|
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,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 12:10:58 +08:00
|
|
|
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(),
|
2026-06-17 15:30:40 +08:00
|
|
|
platform = "android", release = appVersion(), environment = environment,
|
|
|
|
|
sdk = IssueEvent.SdkInfo(SDK_NAME, SDK_VERSION),
|
2026-06-16 12:10:58 +08:00
|
|
|
))
|
|
|
|
|
FunnelTracker.track(name, properties)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 15:30:40 +08:00
|
|
|
fun captureError(error: Throwable, tags: Map<String, Any?> = emptyMap(), fingerprint: String? = null) {
|
2026-06-16 12:10:58 +08:00
|
|
|
if (!isReady()) return
|
2026-06-17 15:30:40 +08:00
|
|
|
val crumbs = synchronized(breadcrumbLock) { breadcrumbBuffer.toList() }
|
|
|
|
|
val fp = fingerprint ?: Fingerprint.compute(
|
|
|
|
|
error.javaClass.simpleName,
|
|
|
|
|
error.message ?: error.javaClass.name,
|
|
|
|
|
error.stackTraceToString(),
|
|
|
|
|
)
|
2026-06-16 12:10:58 +08:00
|
|
|
queue().push(IssueEvent(
|
2026-06-17 15:30:40 +08:00
|
|
|
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,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
2026-06-16 12:10:58 +08:00
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 15:30:40 +08:00
|
|
|
fun warn(message: String, tags: Map<String, Any?> = emptyMap()) {
|
2026-06-16 12:10:58 +08:00
|
|
|
if (logLevel.ordinal > LogLevel.WARN.ordinal) return
|
2026-06-17 15:30:40 +08:00
|
|
|
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,
|
|
|
|
|
))
|
2026-06-16 12:10:58 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-17 15:30:40 +08:00
|
|
|
fun info(message: String, tags: Map<String, Any?> = emptyMap()) {
|
2026-06-16 12:10:58 +08:00
|
|
|
if (logLevel.ordinal > LogLevel.INFO.ordinal) return
|
2026-06-17 15:30:40 +08:00
|
|
|
event("__log_info", tags + ("message" to message))
|
2026-06-16 12:10:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun startCrashCapture() {
|
|
|
|
|
if (crashCaptureStarted) return
|
2026-06-18 10:05:43 +08:00
|
|
|
if (!XuqmSDK.isInitialized()) return
|
|
|
|
|
// bugCollectApiUrl 由远端配置下发,启动时可能尚未到达。
|
|
|
|
|
// 用 platformUrl 推导兜底地址:崩溃文件里会保存此 URL,下次启动时用它上报。
|
|
|
|
|
val logApiUrl = XuqmSDK.bugCollectApiUrl
|
|
|
|
|
?: (XuqmSDK.platformUrl.trimEnd('/') + "/api/bugcollect/")
|
2026-06-16 12:10:58 +08:00
|
|
|
crashCaptureStarted = true
|
|
|
|
|
CrashCapture.start(
|
2026-06-18 09:43:47 +08:00
|
|
|
appContext = XuqmSDK.appContext,
|
2026-06-18 10:05:43 +08:00
|
|
|
logApiUrl = logApiUrl,
|
2026-06-16 12:10:58 +08:00
|
|
|
appKey = XuqmSDK.appKey,
|
|
|
|
|
getUserId = { XuqmSDK.getUserId() },
|
|
|
|
|
)
|
|
|
|
|
queue().uploadPendingCrashes()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun defineFunnel(id: String, steps: List<String>) {
|
|
|
|
|
FunnelTracker.define(id, steps)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 17:39:23 +08:00
|
|
|
private fun isReady() = XuqmSDK.isInitialized() && XuqmSDK.bugCollectEnabled
|
2026-06-16 12:10:58 +08:00
|
|
|
|
|
|
|
|
private fun queue(): LogQueue {
|
|
|
|
|
return queue ?: synchronized(this) {
|
|
|
|
|
queue ?: LogQueue(
|
2026-06-16 17:39:23 +08:00
|
|
|
logApiUrl = XuqmSDK.bugCollectApiUrl ?: "",
|
2026-06-16 12:10:58 +08:00
|
|
|
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")
|
2026-06-17 15:30:40 +08:00
|
|
|
|
|
|
|
|
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",
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-06-16 12:10:58 +08:00
|
|
|
}
|