😽

SentryのIssueをGraphQLオペレーションごとにグルーピングする

2023/06/25に公開

🌼 はじめに

最近 Sentry に飛んでくる Issue をよく見てますが、500エラー(Internal Server Error)同士にグルーピングされてしまう問題で結構悩んでました。

例えば画像アップロードの500エラーが飛んできて、それを resolve する前にユーザー登録の500エラーが飛んできたらその2つがグルーピングされてしまうということです。

同じ500エラーでも違うオペレーションだったら違うIssueにしたいのに、グルーピングされちゃうから確認しづらい!ということで頑張ってグルーピング設定した話をやりたいと思います。

※Apollo Client を使ってます

1. Sentry は fingerprint を基準にグルーピングする

グルーピングを設定するためにはまず Sentry が何を基準にグルーピングしているのかを把握する必要があります。結論から言うと、Sentry は fingerprint を基準に Issue をグルーピングします。

では fingerprint ってなに?ということですが、それに関しては公式サイトで説明しています。

https://docs.sentry.io/product/sentry-basics/grouping-and-fingerprints/

A fingerprint is a way to uniquely identify an event. Sentry sets a fingerprint for you by default, using built-in grouping algorithms based on information such as a stack trace, exception type, and message. Events with the same fingerprint are grouped together.

fingerprint はイベントを一意に識別する方法で、同じ fingerprint を持つイベントは一緒にグルーピングされます

Sentry はデフォルトで fingerprint を設定し、stack trace、 exception type、メッセージなどの情報に基づいた組み込みのグルーピングアルゴリズムを使用します。

要はデフォルトで Sentry がよしなにグルーピングしてくれてるから、特に設定しなくても Issue がグルーピングされるということです。

これは便利ですが、そのよしなに任せて今回のような問題が出てきてるので、これをある程度カスタムしたいと思います。

2. fingerprint をカスタムする方法

fingerprint をカスタムする方法については、公式サイトで Basic Sample を公開しています。

https://docs.sentry.io/platforms/node/usage/sdk-fingerprinting/#basic-example

function makeRequest(method, path, options) {
  return fetch(method, path, options).catch(function (err) {
    Sentry.withScope(function (scope) {
      // group errors together based on their request and response
      scope.setFingerprint([method, path, String(err.statusCode)]);
      Sentry.captureException(err);
    });
  });
}

withScope は新しいスコープを作成し、その中で与えられた操作を実行するメソッドです。そのスコープは操作が終了するか、throw されると自動的に削除されます。

そのコールバック関数の引数である scope は色んなメソッドを持っていて、それを使うと追加情報を送ることができます。setFingerprint もその一つです。

setFingerprint は該当スコープの fingerprint をカスタムします。

上記の例だと method, path, err.statusCode を fingerprint として設定していますね。つまり、この3つの情報を基準にグルーピングするように指定してるということです。

ではこのメソッドを活用してグルーピングを設定してみましょう。

3. Apollo Client の Link を使ってオペレーションごとにグルーピングする

目的としては GraphQL オペレーションごとにグルーピングしたいです。そのために、Apollo Client の Link を活用します。

Linkを使うと Apollo Client と GraphQL サーバー間のデータの流れをカスタマイズできます。その中でも onError はサーバのレスポンスにエラーがないかどうかをチェックするので、今回触っていきます。

Apollo Client の Link について、詳しくは公式サイトをご参考ください。
https://www.apollographql.com/docs/react/api/link/introduction/

では公式に従って、graphQLErrorsの場合に setFingerprint を入れます。

onError(({ graphQLErrors, networkError, operation }) => {
  if (graphQLErrors) {
    for (let err of graphQLErrors) {
      Sentry.withScope((scope) => {
        // オペレーション名を fingerprint として設定する
        scope.setFingerprint([operation.operationName]);
        Sentry.captureException(new Error(err.message), { level: "fatal" });
      });
    }
  }
  
  if (networkError) {
    // ...
  }
});

operation はエラーが発生した GraphQL オペレーションの詳細を提供するオプションで、ここからオペレーション名(operationName)が取得できます。これを setFingerprint の引数の配列に渡せばOK!

…かもしれませんが、実は1つ注意点があります。

setFingerprint は既存の fingerprint に指定した値を追加するものではなくて、上書きします

つまり今のままだた Sentry 側でよしなにやってくれてたデフォルトのグルーピング設定は失われて、オペレーション名だけでグルーピングする設定になってしまいます。

できればデフォルトのグルーピング設定にオペレーション名を追加したいです。

そのためには {{ default }} という変数を引数の配列に追加する必要があります。

onError(({ graphQLErrors, networkError, operation }) => {
  if (graphQLErrors) {
    for (let err of graphQLErrors) {
      Sentry.withScope((scope) => {
-        scope.setFingerprint([operation.operationName]);
+        scope.setFingerprint(["{{ default }}", operation.operationName]);
        Sentry.captureException(new Error(err.message), { level: "fatal" });
      });
    }
  }
  
  if (networkError) {
    // ...
  }
});

setFingerprint の引数の型は string[] なので、 {{ default }} をダブルクォーテーションで囲んであげましょう。

これで既存の fingerprint にオペレーション名を追加できました👏

4. 設定した fingerprint が効いてるかどうか確認する

設定まで完了したらこれがちゃんと効いてるかどうか確認する必要があります。その確認は Sentry のウェブ画面で行います。

Sentry の Issue 詳細画面にアクセスすると、1番下に 「Event Grouping Information 」 というフィールドがあります。その右側に 「Show Detail」 をクリックしたらグルーピング情報が確認できます。

この記事でグルーピング情報確認方法を紹介してるので、ぜひご参考ください。
https://developer.hatenastaff.com/entry/2021/11/08/093000

もし setFingerprint がちゃんと効いてるなら、グルーピング情報の 「Fingerprint values」 というフィールドに以下のような値が入ってるはずです。

[
  {{ default }} ,
  オペレーション名
]

ここまで確認できたらもうばっちりでしょう!

🌷 終わり

余談ですが、このタスクで1番の難問は500エラーを好きなタイミングに再現させることでした( ´∀`)

確認のためにバンバンエラー飛ばしたかったので、こないだテックリードさんに教えてもらった charles というツールを使いました。

https://www.charlesproxy.com/

charles を使うと通信をキャッチしてリクエスト・レスポンスに手を加えることができるので、レスポンスを500エラーのものに上書きして再現しました(超強引)。

便利ツールなのでぜひ使いましょう。設定はこちらを参考にさせていただきました。

https://qiita.com/ishigaki_hige/items/cf86d35e13470a543551

GitHubで編集を提案

Discussion