refactor(bugcollect): tighten public contract

这个提交包含在:
XuqmGroup 2026-07-28 21:24:58 +08:00
父节点 b34b88e29e
当前提交 9493a07b6b
共有 7 个文件被更改,包括 53 次插入51 次删除

查看文件

@ -75,7 +75,7 @@ XuqmSDK.appKey // 当前 appKey
// 平台配置自动初始化完成且宿主已取得隐私授权后: // 平台配置自动初始化完成且宿主已取得隐私授权后:
BugCollect.setLogLevel(LogLevel.INFO) BugCollect.setLogLevel(LogLevel.INFO)
BugCollect.setEnvironment("production") BugCollect.setEnvironment("production")
BugCollect.startCrashCapture() // 开启 UncaughtExceptionHandler // Crash 捕获由平台开关、构建门禁和隐私授权共同驱动,宿主不手动控制。
// 埋点 // 埋点
BugCollect.event("page_view", mapOf("page" to "home")) BugCollect.event("page_view", mapOf("page" to "home"))
@ -114,13 +114,9 @@ YwxMobileApp`/Users/xuqinmin/Projects/TrustProjects/Pad/YwxMobileApp`
## 发版配置 ## 发版配置
`gradle.properties` 中配置: 发布凭据只由 Jenkins Credentials 或开发者本机未入库的 Gradle 属性提供,禁止写入仓库。
```properties 制品发布至 `https://nexus.xuqinmin.com/repository/android-hosted/`,模块版本由根目录
NEXUS_USER=your_username `gradle.properties``SDK_*_VERSION` 与 Jenkins 参数统一管理。
NEXUS_PASSWORD=your_password
```
发布至 `https://nexus.xuqinmin.com/repository/android-hosted/`,版本号在各模块 `build.gradle.kts` 中维护。
## 版本号与发布规则(严禁违反) ## 版本号与发布规则(严禁违反)

查看文件

@ -195,8 +195,11 @@ val result = FileSDK.uploadBytes(
onProgress = { progress -> }, onProgress = { progress -> },
) )
// 上传 File 对象 // 上传 File 对象;缩略图和进度回调均可省略
val result = FileSDK.upload(file = file) val result = FileSDK.upload(
file = file,
onProgress = { progress -> /* 0–100 */ },
)
``` ```
`FileUploadResult` 字段:`url`、`thumbnailUrl`、`hash`、`size`、`originalName`、`mimeType`、`ext` `FileUploadResult` 字段:`url`、`thumbnailUrl`、`hash`、`size`、`originalName`、`mimeType`、`ext`

查看文件

@ -38,10 +38,11 @@ BugCollect.captureError(exception)
|-----|------| |-----|------|
| `BugCollect.setLogLevel(level)` | 设置日志级别(`DEBUG` / `INFO` / `WARN` / `ERROR` | | `BugCollect.setLogLevel(level)` | 设置日志级别(`DEBUG` / `INFO` / `WARN` / `ERROR` |
| `BugCollect.setEnvironment(env)` | 设置环境标签 | | `BugCollect.setEnvironment(env)` | 设置环境标签 |
| `BugCollect.addBreadcrumb(category, message, level?, data?)` | 添加最近操作线索,最多保留 50 条 |
| `BugCollect.event(name, properties?)` | 记录自定义事件 | | `BugCollect.event(name, properties?)` | 记录自定义事件 |
| `BugCollect.captureError(error, metadata?)` | 上报异常 | | `BugCollect.captureError(error, tags?, fingerprint?)` | 上报异常;可选传入业务标签或自定义聚合指纹 |
| `BugCollect.warn(message, metadata?)` | 记录警告 | | `BugCollect.warn(message, tags?)` | 记录警告 |
| `BugCollect.info(message, metadata?)` | 记录信息 | | `BugCollect.info(message, tags?)` | 记录信息 |
| `BugCollect.defineFunnel(id, steps)` | 定义漏斗 | | `BugCollect.defineFunnel(id, steps)` | 定义漏斗 |
### LogLevel ### LogLevel
@ -55,7 +56,7 @@ enum class LogLevel { DEBUG, INFO, WARN, ERROR, NONE }
- **运行门禁**:仅在平台启用且宿主已同意隐私时注册采集器 - **运行门禁**:仅在平台启用且宿主已同意隐私时注册采集器
- **LogQueue**error/fatal 加密持久化,普通事件仅驻留内存;最多 500 条、保留 7 天 - **LogQueue**error/fatal 加密持久化,普通事件仅驻留内存;最多 500 条、保留 7 天
- **CrashCapture**:捕获未处理 Java/Kotlin 异常,加密保存后在下次启动上传 - **CrashCapture**:捕获未处理 Java/Kotlin 异常,加密保存后在下次启动上传
- **Fingerprint**为错误生成指纹(基于 message + stack,服务端去重聚合 - **Fingerprint**基于异常类型、归一化 message 和首个业务堆栈生成指纹,服务端去重聚合
- **FunnelTracker**:客户端维护漏斗进度,服务端跨 session 聚合 - **FunnelTracker**:客户端维护漏斗进度,服务端跨 session 聚合
### 配置 ### 配置

查看文件

@ -147,35 +147,6 @@ object BugCollect {
return true return true
} }
fun captureCrash(error: Throwable, tags: Map<String, Any?> = 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<String, Any?> = emptyMap()) { fun warn(message: String, tags: Map<String, Any?> = emptyMap()) {
if (logLevel.ordinal > LogLevel.WARN.ordinal) return if (logLevel.ordinal > LogLevel.WARN.ordinal) return
if (!isReady()) return if (!isReady()) return
@ -202,8 +173,8 @@ object BugCollect {
event("__log_info", tags + ("message" to message)) event("__log_info", tags + ("message" to message))
} }
/** 注册全局崩溃拦截器(幂等);未满足运行条件时不会启动。 */ /** SDK 状态同步时注册全局崩溃拦截器;宿主无需也不能手动控制生命周期。 */
fun startCrashCapture() { private fun startCrashCapture() {
if (crashCaptureStarted) return if (crashCaptureStarted) return
if (!isReady()) return if (!isReady()) return
runCatching { runCatching {
@ -241,11 +212,6 @@ object BugCollect {
queue?.clearUserContext() queue?.clearUserContext()
} }
/** 上传上次崩溃的加密持久化记录。 */
fun uploadPendingCrashes() {
if (isReady()) runCatching { queue().uploadPendingCrashes() }
}
fun defineFunnel(id: String, steps: List<String>) { fun defineFunnel(id: String, steps: List<String>) {
FunnelTracker.define(id, steps) FunnelTracker.define(id, steps)
} }

查看文件

@ -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),
)
}
}

查看文件

@ -14,5 +14,8 @@ class PublicBridgeApiTest {
assertFalse("onSdkLogin" in methods) assertFalse("onSdkLogin" in methods)
assertFalse("onSdkLogout" in methods) assertFalse("onSdkLogout" in methods)
assertFalse("sendDeveloperTestOnce" in methods) assertFalse("sendDeveloperTestOnce" in methods)
assertFalse("captureCrash" in methods)
assertFalse("startCrashCapture" in methods)
assertFalse("uploadPendingCrashes" in methods)
} }
} }

查看文件

@ -35,7 +35,7 @@ Ed25519 签名,再解密引导配置。V1、未知 keyId、签名错误和包
|-----|------| |-----|------|
| `XuqmSDK.awaitInitialization()` | 等待平台配置拉取coroutine | | `XuqmSDK.awaitInitialization()` | 等待平台配置拉取coroutine |
| `XuqmSDK.isInitialized()` | 检查是否已初始化 | | `XuqmSDK.isInitialized()` | 检查是否已初始化 |
| `XuqmSDK.initializationStatus` | `IDLE / INITIALIZING / READY / DEGRADED / FAILED` | | `XuqmSDK.initializationStatus` | 初始化快照;通过 `.state` 读取 `IDLE / INITIALIZING / READY / DEGRADED / FAILED`,失败原因位于 `.error` |
| `XuqmSDK.setPrivacyConsent(granted)` | 同步宿主隐私授权;SDK 不自行弹窗 | | `XuqmSDK.setPrivacyConsent(granted)` | 同步宿主隐私授权;SDK 不自行弹窗 |
| `XuqmSDK.setUserInfo(info)` | 设置用户信息,触发所有子模块登录 | | `XuqmSDK.setUserInfo(info)` | 设置用户信息,触发所有子模块登录 |
| `XuqmSDK.setUserInfo(null)` | 登出,触发全局登出 | | `XuqmSDK.setUserInfo(null)` | 登出,触发全局登出 |