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 = 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", release = appVersion(), environment = environment, sdk = IssueEvent.SdkInfo(SDK_NAME, SDK_VERSION), )) FunnelTracker.track(name, properties) } 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( 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 = 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, )) } 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, )) } fun info(message: String, tags: Map = 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 val logApiUrl = XuqmSDK.bugCollectApiUrl ?: (XuqmSDK.platformUrl.trimEnd('/') + "/api/bugcollect/") crashCaptureStarted = true CrashCapture.start( appContext = XuqmSDK.appContext, logApiUrl = logApiUrl, appKey = XuqmSDK.appKey, getUserId = { XuqmSDK.getUserId() }, ) queue().uploadPendingCrashes() } /** 上传上次崩溃落盘的文件(远端配置就绪后由 BugCollectInitProvider 自动调用)。 */ fun uploadPendingCrashes() { if (isReady()) queue().uploadPendingCrashes() } fun defineFunnel(id: String, steps: List) { FunnelTracker.define(id, steps) } private fun isReady() = XuqmSDK.isInitialized() && XuqmSDK.bugCollectEnabled private fun queue(): LogQueue { return queue ?: synchronized(this) { queue ?: LogQueue( logApiUrl = XuqmSDK.bugCollectApiUrl ?: "", 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", ) } }