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.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 参数统一管理。
## 版本号与发布规则(严禁违反)

查看文件

@ -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`

查看文件

@ -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 聚合
### 配置

查看文件

@ -147,35 +147,6 @@ object BugCollect {
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()) {
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<String>) {
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("onSdkLogout" 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.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)` | 登出,触发全局登出 |