refactor(bugcollect): tighten public contract
这个提交包含在:
父节点
b34b88e29e
当前提交
9493a07b6b
12
CLAUDE.md
12
CLAUDE.md
@ -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)` | 登出,触发全局登出 |
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户