diff --git a/CLAUDE.md b/CLAUDE.md index 842ed50..324ae03 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,7 +75,7 @@ XuqmSDK.appKey // 当前 appKey // 平台配置自动初始化完成且宿主已取得隐私授权后: BugCollect.setLogLevel(LogLevel.INFO) BugCollect.setEnvironment("production") -BugCollect.startCrashCapture() // 开启 UncaughtExceptionHandler +// Crash 捕获由平台开关、构建门禁和隐私授权共同驱动,宿主不手动控制。 // 埋点 BugCollect.event("page_view", mapOf("page" to "home")) @@ -114,13 +114,9 @@ YwxMobileApp:`/Users/xuqinmin/Projects/TrustProjects/Pad/YwxMobileApp` ## 发版配置 -在 `gradle.properties` 中配置: -```properties -NEXUS_USER=your_username -NEXUS_PASSWORD=your_password -``` - -发布至 `https://nexus.xuqinmin.com/repository/android-hosted/`,版本号在各模块 `build.gradle.kts` 中维护。 +发布凭据只由 Jenkins Credentials 或开发者本机未入库的 Gradle 属性提供,禁止写入仓库。 +制品发布至 `https://nexus.xuqinmin.com/repository/android-hosted/`,模块版本由根目录 +`gradle.properties` 的 `SDK_*_VERSION` 与 Jenkins 参数统一管理。 ## 版本号与发布规则(严禁违反) diff --git a/README.md b/README.md index b5e5d20..560deab 100644 --- a/README.md +++ b/README.md @@ -195,8 +195,11 @@ val result = FileSDK.uploadBytes( onProgress = { progress -> }, ) -// 上传 File 对象 -val result = FileSDK.upload(file = file) +// 上传 File 对象;缩略图和进度回调均可省略 +val result = FileSDK.upload( + file = file, + onProgress = { progress -> /* 0–100 */ }, +) ``` `FileUploadResult` 字段:`url`、`thumbnailUrl`、`hash`、`size`、`originalName`、`mimeType`、`ext` diff --git a/sdk-bugcollect/README.md b/sdk-bugcollect/README.md index dbb03f4..ec917a9 100644 --- a/sdk-bugcollect/README.md +++ b/sdk-bugcollect/README.md @@ -38,10 +38,11 @@ BugCollect.captureError(exception) |-----|------| | `BugCollect.setLogLevel(level)` | 设置日志级别(`DEBUG` / `INFO` / `WARN` / `ERROR`) | | `BugCollect.setEnvironment(env)` | 设置环境标签 | +| `BugCollect.addBreadcrumb(category, message, level?, data?)` | 添加最近操作线索,最多保留 50 条 | | `BugCollect.event(name, properties?)` | 记录自定义事件 | -| `BugCollect.captureError(error, metadata?)` | 上报异常 | -| `BugCollect.warn(message, metadata?)` | 记录警告 | -| `BugCollect.info(message, metadata?)` | 记录信息 | +| `BugCollect.captureError(error, tags?, fingerprint?)` | 上报异常;可选传入业务标签或自定义聚合指纹 | +| `BugCollect.warn(message, tags?)` | 记录警告 | +| `BugCollect.info(message, tags?)` | 记录信息 | | `BugCollect.defineFunnel(id, steps)` | 定义漏斗 | ### LogLevel @@ -55,7 +56,7 @@ enum class LogLevel { DEBUG, INFO, WARN, ERROR, NONE } - **运行门禁**:仅在平台启用且宿主已同意隐私时注册采集器 - **LogQueue**:error/fatal 加密持久化,普通事件仅驻留内存;最多 500 条、保留 7 天 - **CrashCapture**:捕获未处理 Java/Kotlin 异常,加密保存后在下次启动上传 -- **Fingerprint**:为错误生成指纹(基于 message + stack),服务端去重聚合 +- **Fingerprint**:基于异常类型、归一化 message 和首个业务堆栈生成指纹,服务端去重聚合 - **FunnelTracker**:客户端维护漏斗进度,服务端跨 session 聚合 ### 配置 diff --git a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/BugCollect.kt b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/BugCollect.kt index 5af22e5..3386f6d 100644 --- a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/BugCollect.kt +++ b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/BugCollect.kt @@ -147,35 +147,6 @@ object BugCollect { return true } - fun captureCrash(error: Throwable, tags: Map = emptyMap()) { - // SDK 未初始化时直接返回,崩溃事件无法缓存 - if (!XuqmSDK.isInitialized() || !isReady()) return - val crumbs = synchronized(breadcrumbLock) { breadcrumbBuffer.toList() } - enqueue(IssueEvent( - level = "fatal", - platform = "android", - fingerprint = Fingerprint.compute( - error.javaClass.simpleName, - DataSanitizer.sanitizeText(error.message ?: error.javaClass.name), - DataSanitizer.sanitizeText(error.stackTraceToString()), - ), - appKey = XuqmSDK.appKey, - exception = IssueEvent.ExceptionInfo( - type = error.javaClass.simpleName, - value = DataSanitizer.sanitizeText(error.message ?: error.javaClass.name), - stacktrace = DataSanitizer.sanitizeText(error.stackTraceToString()), - ), - breadcrumbs = crumbs, - release = appVersion(), - buildId = buildId(), - environment = environment, - userId = XuqmSDK.getUserId() ?: XuqmSDK.deviceId, - user = IssueEvent.UserInfo(id = XuqmSDK.getUserId() ?: XuqmSDK.deviceId), - device = buildDeviceInfo(), - tags = DataSanitizer.sanitizeMap(tags), - )) - } - fun warn(message: String, tags: Map = emptyMap()) { if (logLevel.ordinal > LogLevel.WARN.ordinal) return if (!isReady()) return @@ -202,8 +173,8 @@ object BugCollect { event("__log_info", tags + ("message" to message)) } - /** 注册全局崩溃拦截器(幂等);未满足运行条件时不会启动。 */ - fun startCrashCapture() { + /** SDK 状态同步时注册全局崩溃拦截器;宿主无需也不能手动控制生命周期。 */ + private fun startCrashCapture() { if (crashCaptureStarted) return if (!isReady()) return runCatching { @@ -241,11 +212,6 @@ object BugCollect { queue?.clearUserContext() } - /** 上传上次崩溃的加密持久化记录。 */ - fun uploadPendingCrashes() { - if (isReady()) runCatching { queue().uploadPendingCrashes() } - } - fun defineFunnel(id: String, steps: List) { FunnelTracker.define(id, steps) } diff --git a/sdk-bugcollect/src/test/java/com/xuqm/sdk/bugcollect/FingerprintTest.kt b/sdk-bugcollect/src/test/java/com/xuqm/sdk/bugcollect/FingerprintTest.kt new file mode 100644 index 0000000..ffeb518 --- /dev/null +++ b/sdk-bugcollect/src/test/java/com/xuqm/sdk/bugcollect/FingerprintTest.kt @@ -0,0 +1,33 @@ +package com.xuqm.sdk.bugcollect + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +class FingerprintTest { + @Test + fun `same error ignores long numeric identifiers and later stack frames`() { + val first = Fingerprint.compute( + "IllegalStateException", + "order 202607280001 failed", + "at com.example.Order.submit(Order.kt:12)\nat kotlinx.coroutines.Worker.run(Worker.kt:20)", + ) + val second = Fingerprint.compute( + "IllegalStateException", + "order 202607289999 failed", + "at com.example.Order.submit(Order.kt:12)\nat kotlinx.coroutines.Other.run(Other.kt:99)", + ) + + assertEquals(first, second) + } + + @Test + fun `exception type remains part of grouping identity`() { + val stack = "at com.example.Order.submit(Order.kt:12)" + + assertNotEquals( + Fingerprint.compute("IllegalStateException", "failed", stack), + Fingerprint.compute("IllegalArgumentException", "failed", stack), + ) + } +} diff --git a/sdk-bugcollect/src/test/java/com/xuqm/sdk/bugcollect/PublicBridgeApiTest.kt b/sdk-bugcollect/src/test/java/com/xuqm/sdk/bugcollect/PublicBridgeApiTest.kt index 1e4bb6b..e9d1e85 100644 --- a/sdk-bugcollect/src/test/java/com/xuqm/sdk/bugcollect/PublicBridgeApiTest.kt +++ b/sdk-bugcollect/src/test/java/com/xuqm/sdk/bugcollect/PublicBridgeApiTest.kt @@ -14,5 +14,8 @@ class PublicBridgeApiTest { assertFalse("onSdkLogin" in methods) assertFalse("onSdkLogout" in methods) assertFalse("sendDeveloperTestOnce" in methods) + assertFalse("captureCrash" in methods) + assertFalse("startCrashCapture" in methods) + assertFalse("uploadPendingCrashes" in methods) } } diff --git a/sdk-core/README.md b/sdk-core/README.md index 251e337..7530b85 100644 --- a/sdk-core/README.md +++ b/sdk-core/README.md @@ -35,7 +35,7 @@ Ed25519 签名,再解密引导配置。V1、未知 keyId、签名错误和包 |-----|------| | `XuqmSDK.awaitInitialization()` | 等待平台配置拉取(coroutine) | | `XuqmSDK.isInitialized()` | 检查是否已初始化 | -| `XuqmSDK.initializationStatus` | `IDLE / INITIALIZING / READY / DEGRADED / FAILED` | +| `XuqmSDK.initializationStatus` | 初始化快照;通过 `.state` 读取 `IDLE / INITIALIZING / READY / DEGRADED / FAILED`,失败原因位于 `.error` | | `XuqmSDK.setPrivacyConsent(granted)` | 同步宿主隐私授权;SDK 不自行弹窗 | | `XuqmSDK.setUserInfo(info)` | 设置用户信息,触发所有子模块登录 | | `XuqmSDK.setUserInfo(null)` | 登出,触发全局登出 |