目录
1. Sentry 事件分组机制概述
1.1 分组原理
Sentry 使用 指纹(Fingerprint)机制 来确定哪些事件应该被归入同一个 Issue。具有相同 fingerprint 的事件会被合并到同一个 Issue 中。
1.2 Fingerprint 的数据结构
Fingerprint 是一个 字符串列表(List<String>),例如:
["exception:NullPointerException", "module:MyClass", "function:method1"]
1.3 分组流程
用户代码捕获异常
↓
Sentry.captureException()
↓
Scope.applyToEvent() - 应用 Scope 中的 fingerprint
↓
SentryEvent.setFingerprint() - 设置到事件对象
↓
序列化为 JSON(包含 fingerprint)
↓
发送到 Sentry 服务器
↓
服务器根据 fingerprint 进行分组
1.4 源码实现位置
SDK 端:
-
io.sentry.Scope- 存储 fingerprint -
io.sentry.SentryEvent- 事件对象中的 fingerprint 字段 -
io.sentry.android.core.AnrV2EventProcessor- ANR 事件的 fingerprint 设置
服务器端:
- Sentry 服务器端使用 Python 实现分组算法
- 基于 fingerprint 和默认分组规则进行事件合并
2. Sentry SDK 源码中的 Fingerprint 实现
2.1 Scope 类中的定义
位置: io.sentry.Scope
public class Scope {
private @Nullable List<String> fingerprint;
public void setFingerprint(@Nullable List<String> fingerprint) {
this.fingerprint = fingerprint;
}
public @Nullable List<String> getFingerprint() {
return fingerprint;
}
// 将 Scope 中的 fingerprint 应用到事件
public @Nullable SentryEvent applyToEvent(
@NotNull SentryEvent event,
@Nullable Hint hint
) {
if (this.fingerprint != null && !this.fingerprint.isEmpty()) {
event.setFingerprint(this.fingerprint);
}
return event;
}
}
2.2 SentryEvent 类中的定义
位置: io.sentry.SentryEvent
public class SentryEvent {
private @Nullable List<String> fingerprint;
public void setFingerprint(@Nullable List<String> fingerprint) {
this.fingerprint = fingerprint;
}
public @Nullable List<String> getFingerprint() {
return fingerprint;
}
}
2.3 SDK 源码中调用 setFingerprint 的位置
2.3.1 ANR 事件的分组(AnrV2EventProcessor)
位置: io.sentry.android.core.AnrV2EventProcessor.java
关键代码:
@SuppressWarnings("unchecked")
private void setFingerprints(final @NotNull SentryEvent event, final @NotNull Object hint) {
// 1. 首先尝试从持久化的 Scope 中读取 fingerprint
final List<String> fingerprint =
(List<String>) readFromDisk(options, FINGERPRINT_FILENAME, List.class);
if (event.getFingerprints() == null) {
event.setFingerprints(fingerprint);
}
// 2. 如果没有设置 fingerprint,则使用默认分组逻辑
// 注释说明:Sentry 服务器端还没有提供默认的 fingerprint 规则能力,
// 所以在 SDK 端实现,将后台和前台 ANR 分开分组,即使它们有相似的堆栈
final boolean isBackgroundAnr = isBackgroundAnr(hint);
if (event.getFingerprints() == null) {
event.setFingerprints(
Arrays.asList("{{ default }}", isBackgroundAnr ? "background-anr" : "foreground-anr"));
}
}
private boolean isBackgroundAnr(final @NotNull Object hint) {
if (hint instanceof AbnormalExit) {
final String abnormalMechanism = ((AbnormalExit) hint).mechanism();
return "anr_background".equals(abnormalMechanism);
}
return false;
}
说明:
- ANR 事件会区分前台和后台
- Fingerprint 格式:
["{{ default }}", "background-anr"]或["{{ default }}", "foreground-anr"] -
{{ default }}表示使用服务器端默认分组算法
2.3.2 用户代码中的设置
位置: 项目代码 comm_lib/src/main/java/hy/sohu/com/comm_lib/utils/ANRTraceFileManager.kt
Sentry.setFingerprint(mutableListOf(anrInfo.processName, anrInfo.description?:""))
说明:
- 这是项目中的自定义设置
- 使用进程名和描述作为 fingerprint
- 所有具有相同进程名和描述的 ANR 会被合并
3. 不同类型错误的分组依据
3.1 ANR (Application Not Responding)
分组依据:
- 用户设置的 fingerprint(最高优先级)
- Scope 中持久化的 fingerprint
-
SDK 自动设置:
["{{ default }}", "background-anr"]或["{{ default }}", "foreground-anr"] - 服务器端默认分组(如果未设置)
示例:
// 示例 1:前台 ANR
Fingerprint: ["{{ default }}", "foreground-anr"]
堆栈: MainActivity.onCreate() -> MyClass.processData()
// 示例 2:后台 ANR
Fingerprint: ["{{ default }}", "background-anr"]
堆栈: BackgroundService.onStart() -> MyClass.processData()
// 即使堆栈相似,前台和后台 ANR 会被分开分组
3.2 Java Crash
分组依据(服务器端默认):
- 异常类型(Exception Type)
- 异常消息(Exception Value/Message)
-
堆栈顶层框架(Top Stack Frames,前 3-5 层 in-app frames)
- 模块名(Module)
- 函数名(Function)
- 文件名(Filename)
- 行号通常被忽略
- 错误机制(Mechanism)
示例:
// 示例 1:相同异常类型和顶层框架
Exception 1: NullPointerException at MyClass.method1(MyClass.java:123)
Exception 2: NullPointerException at MyClass.method1(MyClass.java:456)
// ✅ 会被合并到同一个 Issue
// 分组依据:异常类型相同 + 顶层框架相同(忽略行号差异)
// 示例 2:异常类型不同
Exception 1: NullPointerException at MyClass.method1()
Exception 2: IllegalArgumentException at MyClass.method1()
// ❌ 会被分成不同的 Issue
// 分组依据:异常类型不同
// 示例 3:顶层框架不同
Exception 1: NullPointerException at MyClass.method1()
Exception 2: NullPointerException at MyClass.method2()
// ❌ 会被分成不同的 Issue
// 分组依据:顶层框架不同
3.3 Native Crash
分组依据(服务器端默认):
-
崩溃信号(Signal):
SIGSEGV、SIGABRT、SIGBUS、SIGFPE等 - 崩溃地址(Crash Address):发生崩溃的内存地址
-
Native 堆栈信息(如果可用):
- 库名(Library):如
libc.so、libapp.so、libhwui - 函数名(Function):符号化后的函数名
- 偏移量(Offset):在库中的偏移
- 库名(Library):如
-
错误机制(Mechanism):通常为
"NativeCrash"
示例:
// 示例 1:相同的信号和地址
Crash 1: SIGSEGV at 0x12345678 in libapp.so (function: my_crash_function)
Crash 2: SIGSEGV at 0x12345678 in libapp.so (function: my_crash_function)
// ✅ 会被合并到同一个 Issue
// 分组依据:信号相同 + 地址相同 + 函数相同
// 示例 2:信号不同
Crash 1: SIGSEGV at 0x12345678
Crash 2: SIGABRT at 0x87654321
// ❌ 会被分成不同的 Issue
// 分组依据:信号不同
// 示例 3:相同模块但不同函数
Crash 1: SIGSEGV in libhwui (function: SkPathRef::growForVerbsInPath)
Crash 2: SIGSEGV in libhwui (function: <unknown>)
// ⚠️ 可能被合并(如果第二个未符号化,可能使用模块级别匹配)
// 分组依据:信号相同 + 模块相同
3.4 实际案例分析
案例:两个 SIGSEGV 被合并的原因
事件 1:
错误类型: SIGSEGV
堆栈:
1. libc +0x09430c <unknown>
2. libhwui +0x480564 SkPathRef::growForVerbsInPath
3. libhwui +0x480054 SkPath::addPath
4. libhwui +0x5a3518 <unknown>
5. boot-framework.oat +0x1eee18 <unknown>
事件 2:
错误类型: SIGSEGV
堆栈:
1. libhwui +0x37484c <unknown>
2. libhwui +0x36ab68 <unknown>
3. boot-framework.oat +0x23be44 <unknown>
合并原因:
- ✅ 相同的错误类型:
SIGSEGV - ✅ 相同的机制:
signalhandler - ✅ 都涉及相同的模块:
libhwui+boot-framework.oat - ✅ 系统框架(
libc)可能被忽略,导致顶层应用框架相似 - ⚠️ 符号化状态不一致,可能使用模块级别的模糊匹配
4. Sentry 服务器端默认分组逻辑
4.1 默认分组算法({{ default }})
当事件包含 "{{ default }}" 时,服务器端使用默认分组算法生成 fingerprint。
4.2 Java Crash 的服务器端分组
分组依据(按优先级):
-
异常类型(Exception Type)
- 完全匹配:
NullPointerException≠IllegalArgumentException - 这是分组的第一要素
- 完全匹配:
-
异常消息(Exception Value/Message)
- 部分匹配或规范化处理
- 例如:
"Attempt to invoke virtual method on null object"会被规范化
-
堆栈顶层框架(Top Stack Frames)
- 通常取前 3-5 层 in-app frames
- 提取信息:
- 模块名(Module):类名
- 函数名(Function):方法名
- 文件名(Filename):文件名(不含路径)
- 行号(Lineno):可能被忽略或模糊化
-
错误机制(Mechanism)
- 如
"UncaughtException"、"UnhandledPromiseRejection"等
- 如
分组算法(伪代码):
def generate_default_fingerprint(event):
fingerprint = []
# 1. 异常类型
if event.exception:
fingerprint.append(f"exception:{event.exception.type}")
# 2. 异常消息(规范化)
if event.exception.value:
normalized_message = normalize_message(event.exception.value)
fingerprint.append(f"message:{normalized_message}")
# 3. 堆栈顶层 in-app frames(前 3-5 层)
if event.exception.stacktrace:
in_app_frames = [f for f in event.exception.stacktrace.frames if f.in_app]
top_frames = in_app_frames[:5] # 取前 5 层
for frame in top_frames:
# 模块 + 函数名(忽略行号)
if frame.module and frame.function:
fingerprint.append(f"module:{frame.module}")
fingerprint.append(f"function:{frame.function}")
# 4. 错误机制
if event.exception.mechanism:
fingerprint.append(f"mechanism:{event.exception.mechanism.type}")
return fingerprint if fingerprint else ["{{ default }}"]
4.3 Native Crash 的服务器端分组
分组依据:
-
崩溃信号(Signal)
-
SIGSEGV、SIGABRT、SIGBUS、SIGFPE等 - 不同信号通常分开分组
-
-
崩溃地址(Crash Address)
- 发生崩溃的内存地址
- 相似地址可能被分组(取决于配置)
-
Native 堆栈信息
- 如果可用,分析 native 堆栈
- 提取:
-
库名(Library):如
libc.so、libapp.so、libhwui - 函数名(Function):符号化后的函数名
- 偏移量(Offset):在库中的偏移
-
库名(Library):如
-
错误机制
- 通常为
"NativeCrash"或类似值
- 通常为
4.4 堆栈框架的处理规则
In-App vs System Frames:
-
In-App Frames(应用内框架)
- 属于应用代码的框架
- 权重更高,优先用于分组
- 通过
in_app标记识别
-
System Frames(系统框架)
- 系统库、第三方库的框架
- 权重较低,可能被忽略或模糊化
框架选择策略:
堆栈示例:
1. android.os.Handler.dispatchMessage() [系统框架,可能被忽略]
2. com.example.MyActivity.onCreate() [In-App,重要]
3. com.example.MyClass.processData() [In-App,重要]
4. com.example.Utils.helper() [In-App,重要]
5. java.lang.Thread.run() [系统框架,可能被忽略]
分组时主要考虑:MyActivity.onCreate() + MyClass.processData() + Utils.helper()
4.5 分组规则的特殊处理
行号处理:
- 行号可能被忽略或模糊化
- 相同文件和方法但不同行号的事件可能被合并
消息规范化:
- 移除动态内容(如 ID、时间戳)
- 统一大小写和格式
框架过滤:
- 忽略系统框架
- 优先使用 in-app 框架
5. 分组依据总结表
| 错误类型 | Fingerprint 设置位置 | 分组依据 | 优先级 | 示例 |
|---|---|---|---|---|
| ANR | AnrV2EventProcessor.setFingerprints() |
1. 用户设置的 fingerprint 2. Scope 持久化的 fingerprint 3. ["{{ default }}", "background-anr"] 或 ["{{ default }}", "foreground-anr"]4. 服务器端默认分组 |
用户设置 > Scope > SDK 自动 > 服务器默认 | ["{{ default }}", "foreground-anr"] |
| Java Crash | 无(使用服务器端默认) | 1. 异常类型(Exception Type) 2. 异常消息(Exception Value) 3. 堆栈顶层 in-app frames(前 3-5 层) - 模块名(Module) - 函数名(Function) - 文件名(Filename) - 行号通常被忽略 4. 错误机制(Mechanism) |
异常类型 > 异常消息 > 堆栈框架 > 机制 | ["exception:NullPointerException", "module:MyClass", "function:method1"] |
| Native Crash | 无(使用服务器端默认) | 1. 崩溃信号(Signal) 2. 崩溃地址(Crash Address) 3. Native 堆栈信息(如果可用) - 库名(Library) - 函数名(Function) - 偏移量(Offset) 4. 错误机制(Mechanism) |
信号 > 地址 > 堆栈信息 > 机制 | ["signal:SIGSEGV", "library:libhwui", "function:SkPathRef::growForVerbsInPath"] |
| 用户自定义 |
Scope.setFingerprint()或 Sentry.setFingerprint()
|
用户指定的字符串列表 | 最高优先级 | ["custom-key", "value1", "value2"] |
5.1 分组优先级总结
1. 用户手动设置的 fingerprint(最高优先级)
↓
2. Scope 中持久化的 fingerprint
↓
3. SDK 自动设置的 fingerprint(如 ANR 的前后台区分)
↓
4. 服务器端默认分组算法({{ default }})
5.2 特殊标记说明
-
{{ default }}:告诉服务器使用默认分组算法 -
in_app:标记框架是否为应用内代码(权重更高) -
<unknown>:未符号化的函数名(可能使用模块级别匹配)
5.3 分组示例对比
| 场景 | 事件 1 | 事件 2 | 是否合并 | 原因 |
|---|---|---|---|---|
| Java Crash - 相同异常类型和框架 |
NullPointerExceptionat MyClass.method1()
|
NullPointerExceptionat MyClass.method1()
|
✅ 是 | 异常类型相同 + 顶层框架相同 |
| Java Crash - 异常类型不同 |
NullPointerExceptionat MyClass.method1()
|
IllegalArgumentExceptionat MyClass.method1()
|
❌ 否 | 异常类型不同 |
| Java Crash - 框架不同 |
NullPointerExceptionat MyClass.method1()
|
NullPointerExceptionat MyClass.method2()
|
❌ 否 | 顶层框架不同 |
| Native Crash - 相同信号和模块 |
SIGSEGV in libhwui
|
SIGSEGV in libhwui
|
✅ 是 | 信号相同 + 模块相同 |
| Native Crash - 信号不同 |
SIGSEGV in libhwui
|
SIGABRT in libhwui
|
❌ 否 | 信号不同 |
| ANR - 前后台区分 | 前台 ANR | 后台 ANR | ❌ 否 | SDK 设置了不同的 fingerprint |
| ANR - 相同类型 | 前台 ANR #1 | 前台 ANR #2 | ✅ 是 | 相同的 fingerprint |
6. 最佳实践建议
6.1 何时使用自定义 Fingerprint
-
需要合并不同堆栈的相似错误
// 例如:所有数据库连接错误合并到一个 issue Sentry.setFingerprint(listOf("database-error", error.message)) -
需要分开相同堆栈的不同场景
// 例如:根据用户类型分开 Sentry.setFingerprint(listOf("payment-error", userType))
6.2 避免的问题
-
不要使用动态值
// ❌ 错误:每个事件都会创建新的 issue Sentry.setFingerprint(listOf("error", System.currentTimeMillis().toString())) // ✅ 正确:使用稳定的标识符 Sentry.setFingerprint(listOf("error", errorType)) -
不要过度分组
// ❌ 错误:所有错误都合并到一个 issue Sentry.setFingerprint(listOf("all-errors"))
6.3 调试分组问题
-
查看事件的 fingerprint
- 在 Sentry 界面查看事件的 fingerprint 值
- 确认是否包含
"{{ default }}"或自定义值
-
检查符号化状态
- 确保 native crash 有正确的符号文件
- 检查堆栈是否完全符号化
-
验证分组规则
- 检查 Sentry 项目设置中的分组规则
- 确认是否有自定义的分组配置