Sentry 事件分组机制说明文档

目录

  1. Sentry 事件分组机制概述
  2. Sentry SDK 源码中的 Fingerprint 实现
  3. 不同类型错误的分组依据
  4. Sentry 服务器端默认分组逻辑
  5. 分组依据总结表

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)

分组依据:

  1. 用户设置的 fingerprint(最高优先级)
  2. Scope 中持久化的 fingerprint
  3. SDK 自动设置["{{ default }}", "background-anr"]["{{ default }}", "foreground-anr"]
  4. 服务器端默认分组(如果未设置)

示例:

// 示例 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

分组依据(服务器端默认):

  1. 异常类型(Exception Type)
  2. 异常消息(Exception Value/Message)
  3. 堆栈顶层框架(Top Stack Frames,前 3-5 层 in-app frames)
    • 模块名(Module)
    • 函数名(Function)
    • 文件名(Filename)
    • 行号通常被忽略
  4. 错误机制(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

分组依据(服务器端默认):

  1. 崩溃信号(Signal):SIGSEGVSIGABRTSIGBUSSIGFPE
  2. 崩溃地址(Crash Address):发生崩溃的内存地址
  3. Native 堆栈信息(如果可用):
    • 库名(Library):如 libc.solibapp.solibhwui
    • 函数名(Function):符号化后的函数名
    • 偏移量(Offset):在库中的偏移
  4. 错误机制(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>

合并原因:

  1. ✅ 相同的错误类型:SIGSEGV
  2. ✅ 相同的机制:signalhandler
  3. ✅ 都涉及相同的模块:libhwui + boot-framework.oat
  4. ✅ 系统框架(libc)可能被忽略,导致顶层应用框架相似
  5. ⚠️ 符号化状态不一致,可能使用模块级别的模糊匹配

4. Sentry 服务器端默认分组逻辑

4.1 默认分组算法({{ default }}

当事件包含 "{{ default }}" 时,服务器端使用默认分组算法生成 fingerprint。

4.2 Java Crash 的服务器端分组

分组依据(按优先级):

  1. 异常类型(Exception Type)

    • 完全匹配:NullPointerExceptionIllegalArgumentException
    • 这是分组的第一要素
  2. 异常消息(Exception Value/Message)

    • 部分匹配或规范化处理
    • 例如:"Attempt to invoke virtual method on null object" 会被规范化
  3. 堆栈顶层框架(Top Stack Frames)

    • 通常取前 3-5 层 in-app frames
    • 提取信息:
      • 模块名(Module):类名
      • 函数名(Function):方法名
      • 文件名(Filename):文件名(不含路径)
      • 行号(Lineno):可能被忽略或模糊化
  4. 错误机制(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 的服务器端分组

分组依据:

  1. 崩溃信号(Signal)

    • SIGSEGVSIGABRTSIGBUSSIGFPE
    • 不同信号通常分开分组
  2. 崩溃地址(Crash Address)

    • 发生崩溃的内存地址
    • 相似地址可能被分组(取决于配置)
  3. Native 堆栈信息

    • 如果可用,分析 native 堆栈
    • 提取:
      • 库名(Library):如 libc.solibapp.solibhwui
      • 函数名(Function):符号化后的函数名
      • 偏移量(Offset):在库中的偏移
  4. 错误机制

    • 通常为 "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 - 相同异常类型和框架 NullPointerException
at MyClass.method1()
NullPointerException
at MyClass.method1()
✅ 是 异常类型相同 + 顶层框架相同
Java Crash - 异常类型不同 NullPointerException
at MyClass.method1()
IllegalArgumentException
at MyClass.method1()
❌ 否 异常类型不同
Java Crash - 框架不同 NullPointerException
at MyClass.method1()
NullPointerException
at 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

  1. 需要合并不同堆栈的相似错误

    // 例如:所有数据库连接错误合并到一个 issue
    Sentry.setFingerprint(listOf("database-error", error.message))
    
  2. 需要分开相同堆栈的不同场景

    // 例如:根据用户类型分开
    Sentry.setFingerprint(listOf("payment-error", userType))
    

6.2 避免的问题

  1. 不要使用动态值

    // ❌ 错误:每个事件都会创建新的 issue
    Sentry.setFingerprint(listOf("error", System.currentTimeMillis().toString()))
    
    // ✅ 正确:使用稳定的标识符
    Sentry.setFingerprint(listOf("error", errorType))
    
  2. 不要过度分组

    // ❌ 错误:所有错误都合并到一个 issue
    Sentry.setFingerprint(listOf("all-errors"))
    

6.3 调试分组问题

  1. 查看事件的 fingerprint

    • 在 Sentry 界面查看事件的 fingerprint 值
    • 确认是否包含 "{{ default }}" 或自定义值
  2. 检查符号化状态

    • 确保 native crash 有正确的符号文件
    • 检查堆栈是否完全符号化
  3. 验证分组规则

    • 检查 Sentry 项目设置中的分组规则
    • 确认是否有自定义的分组配置

7. 参考资料

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容