SentryのIssue Groupingの仕組みについて解説

## Sentryとは
Sentryとは、アプリケーションで発生したエラーを監視するためのツールです。発生したエラーを通知してくれるだけでなく、エラー発生時のコード位置やエラーメッセージなど、さまざまな情報をダッシュボードから確認でき、アプリケーションの不具合を早期に発見するために活用されます。

https://sentry.io/welcome/

## Issue Grouping
Sentryには、類似のエラーを同一グループとしてまとめる機能があります。デフォルトの設定でもそれなりに賢くエラーを分類してくれるのですが、これではでは足りない場合もそれなりにあり、その場合はグルーピングルールをカスタマイズする必要があります。

適切にカスタマイズするためには、Sentryが具体的にどのようにエラーを分類するのかを理解する必要があるのですが、やや複雑で難しいと感じたため、今回記事にまとめてみようと思いました。

## なぜ分類が重要か
アプリケーションのエラーは、重要度や発生頻度など、特性が原因によって異なることが多く、これらの特性に応じた通知設定が重要です。具体的には、以下のようなものです。

**一度でも発生するとまずいエラー**
ハンドリングできていない例外や実装の不具合など、ビジネスクリティカルなエラーなどがこれに該当します。一度でも発生すれば通知して欲しいです。

**通常低発生で発生するが、過剰に発生するとまずいもの**
データベースのコネクションエラーなどは、正常時でも外部要因などで低頻度発生する可能性がありますが、過剰に発生する場合はネットワークやアプリケーションに深刻な問題が発生している可能性があるので通知して欲しいです。

**無害なもの**
こちらについては、そもそも通知してほしくないです。

エラーを適切に分類できていると、Sentryで上記のような設定ができますが、適切に分類できていないとそうはいきません。既知の問題にも関わらず未知の問題と判定されることで、毎回通知されてしまい、本当に見る必要のある重要なエラーが埋もれてしまうなどの危険性があります。

ここからは本題であるグルーピングの仕組みを説明していきます。

## グルーピングの流れ
![スクリーンショット 2023-07-15 5.03.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324326/fb473a2f-43af-be6b-6f65-4f0405fadce7.png)

Sentryがアプリケーションエラーを分類する大まかな流れは上記のようになります。

アプリケーションからmessage, stacktrace, Exceptionの3種類のデータが送られ、Fingerprint Rule, Default Grouping Rule, Stack Trace Ruleの仕組みによりFingerprintというデータを生成します。

Fingerprintは、複数の文字列により構成される値で、同一のFingerprintを生成したエラーが同じエラーとして分類されます。


## Default Grouping Rule
Sentryがデフォルトで実行するグルーピングルールです。message, Stack Trace, Exceptionの情報からFingerprintを生成します。

![スクリーンショット 2023-07-15 5.47.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324326/19392c9d-6baf-ff19-19e4-369e20bb4624.png)

Stack Trace, Exception, messageの順で評価されます。

これらの値は常にすべて送信されるわけではなく、Stack Traceが利用できなければExceptionが利用され、Stack TraceもExceptionも利用できない場合はmessageが利用される、というように段階的に評価されます。

### Stack Trace
- 最も優先的に評価される
- Stack Traceが存在する場合、messageはグルーピングに寄与しない

関数名やモジュール名などの情報がスタックトレースとしてSentryへ送信されます。Stack Traceには通常、たくさんの情報が付与されますが、必ずしもすべての値がグルーピングに利用されるわけではありません。一部の情報は組み込みアルゴリズムで除去されるほか、後述するStack Trace Ruleで明示的に除去したい値を指定することができます。

最終的に残ったスタックトレースのフレームがすべて同じ場合、同一のFingerprintが生成されます。

スタックトレースの例
![スクリーンショット 2023-07-15 5.50.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324326/f3e74641-244c-88f8-e163-c4f7a9f9a18f.png)


### Exception
- Stack Traceが利用できないがExceptionが利用できる場合に評価される
- Exceptionが存在する場合、messageはグルーピングに寄与しない

Strack Traceの次に評価されるのはExceptionです。Exceptionとは、その名の通り例外のことで、アプリケーションで発生した例外の型と、例外に渡した引数ががいとうします。

以下に例外を発生するコードと、このとき生成されるExceptionの値を示します。


```python
try:
    raise ValueError(
        "スタックトレースはいつ送信されるんやろか",
        "hoge","huga"
    )
except Exception as e:
    logger.exception("このメッセージはグルーピングに寄与しない")
```

![スクリーンショット 2023-07-15 6.05.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324326/19bd6e93-0fc5-d659-a6b3-4e482f63d899.png)

type, value両方の値が同じ場合、同じFingerprintが生成されます。

### message
- Stack Trace, Exceptionのどちらも利用できない場合に評価される
- message 文字列が一致した場合に同一Issueと見なされる
- パラメータなどをロギングする場合、プレースホルダーを使うとよい

Stack Trace, Exceptionのどちらも利用できない場合、messageが利用されます。これはエラーログとしてloggerに渡したメッセージがこれに該当します。

ここで一つ注意すべきなのは、ログにパラメータの情報を含めたい場合です。以下のように記述するとSentryに送信される情報にパラメータの値が含まれてしまいます。この場合、パラメータの値が変わるたびに別Issueとして分類されてしまうため、よくない可能性があります。

(注: Pythonです)
```python
logger.error("エラーメッセージ, param: {}".format("hoge"))
```
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324326/08552a60-bc58-5a7a-6d05-a6b0d852cb77.png width=500>

これは、以下のようにプレースホルダーを利用することで回避できます。この場合、Sentryに送信されるのは"エラーメッセージ, param: %s"なので、別のパラメータが渡された場合でも同一Issueに分類することができます。

```python
logger.error("エラーメッセージ, param: %s", "hoge")
```
<img src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324326/d12442e2-d426-f81c-6633-65b8957db40b.png width=500>

## Stack Trace Rule
Stack Trace Ruleは、グルーピングをカスタマイズするための仕組みの一つです。Default Grouping Ruleで利用されるスタックトレースから不要な情報を除去するルールを記述することができます。

サードパーティのライブラリなど、グルーピングに関与させたくないモジュールや関数がある場合、Stack Trace Ruleを記述することでこれを除去できます。

具体的な例をしめします。以下のようなスタックトレースがSentryに送信されるとし、一番下のフレームをグルーピングに関与させたくないとします。
![スクリーンショット 2023-07-15 6.22.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324326/6e7fcd23-8590-fb94-932b-bb8cb7d20b99.png)


以下のように記述することで、これを達成できます。これは、関数名が ”getStackTrace” のフレームを除去するという意味になります。
```
stack.function:getStackTrace -group
```

上記のルールを適用することで、スタックトレースは以下のように変化します。
![スクリーンショット 2023-07-15 6.23.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324326/04009fd1-ad21-d5f5-734d-d0565687aca1.png)

無事に該当フレームを除去することができました。

## Fingerprint Rule
Fingerprintルールは、グルーピングをカスタマイズするもう一つの仕組みです。message, Exception, Stack TraceがFingerprintを生成するルールを明示的に記述することができます。

以下に具体的な例を示します。
```python
# エラーの型がHogeErrorの場合にsystem-downというFingerprintを生成する
error.type:HogeError -> system-down

# messageの先頭が'hoge error:'の場合にhoge-errorというFingerprintを生成する
message:"hoge error:*" -> hoge-error

# 特殊文字列を指定した場合は少し特殊な動きをする
message:"huga error:*" -> huga-error, {{ default }}
```

一番下の例は少し特殊で、{{ default }}という特殊文字を利用しています。この場合生成されるのは、”huga-error”というFingerprintと「Default Grouping Ruleで生成されるFingerprint」です。

huga-errorというFingerprintをもち、なおかつデフォルトルールで同一とみなされた場合にのみ、同一Issueとみなされます。

このような特殊文字は他にも存在します。詳細は[ドキュメント](https://docs.sentry.io/product/data-management-settings/event-grouping/fingerprint-rules/)を参考にしてください。

## Finger PrintをSDKから指定することもできる
Fingerprintについては、前述のSentry UIから設定する他に、SDKで設定することもできます。

SDKからFingerprintを指定した場合も、UIから設定した場合と同様に評価されます。
```python
try:
    # ...
except Exception as e:
    with sentry_sdk.push_scope() as scope:
        if type(e) is HogeError:
            scope.fingerprint = ["hoge", "{{ default }}"]
        sentry_sdk.capture_exception(e)
# HogeErrorが発生した場合、["hoge", "{{ default }}"]のFingerprintを付与する
```

## まとめ
今回はSentryのグルーピングルールについて解説してみました。何かの参考になれば嬉しいです。


## 補足1: UI上でどのようにグルーピングされたかを確認する方法

SentryのIssue画面の下部にイベントのグループ化情報という項目があり、ここを見ると具体的にどの情報がグルーピングに利用されたのかを確認できます。

期待通りにグルーピングされない場合はまずここを確認するとよいです。

![スクリーンショット 2023-07-15 6.50.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324326/080d8834-2c20-b66d-8906-bb0424498db3.png)


## 補足2: 利用するSentry統合によって挙動が若干異なる
以前自分がハマった事例を載せておきます。

似た動きをするSentry統合でも、若干挙動が異なる場合があるので注意が必要です。
Pythonのlogging統合とjavaのlog4j統合は、それぞれアプリケーションログをSentryに送信するためのものなのですが、若干挙動が異なりました。

Pythonのlogging統合は、例外情報を付与した場合のみスタックトレースを送信しますが、log4j統合は常にスタックトレースを送信します。これによって、似たような書き方をしてもなぜかjavaだけ上手くグルーピングされない、というようなことがありました。

![スクリーンショット 2023-07-15 6.44.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324326/95693e86-659e-7870-1f31-9263b87b580e.png)

![スクリーンショット 2023-07-15 6.46.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324326/4dd76564-0e7d-864e-8c8a-6058045b82b5.png)