ApolloServer + ApolloClientのエラー通知

## はじめに
若干ニッチな内容ですが、`ApolloServer` + `ApolloClient` を使ったwebアプリケーションにおけるエラーの通知に関して考えてみます。

## 前提
1. フロントエンドのアプリケーションは [ApolloClient](https://www.npmjs.com/package/@apollo/client) を使いバックエンドのGraphQLサーバーと通信、バックエンドは [ApollServer](https://www.npmjs.com/package/apollo-server) をベースにGraphQLリクエストを受け付け、各resolverにて処理を実行した後に適切なレスポンスをフロントエンドに返却します。
1. フロントエンド・バックエンドそれぞれに `errorLogger` というエラー通知をするために関数を用意し、チーム内で決めた手法でエラーログの送信を行います（Sentry、CloudWatchなど）。

## バックエンド
以下のような形のエラー通知が考えられます。

1. resolverごとにエラー通知を行う 
1. ApolloServerの `didEncounterErrors` イベントを活用する

### 1. resolverごとにエラー通知を行う 
親にあたるresolverまで子/孫の関数のエラーを伝播させ `errorLogger` を呼ぶ、もしくはresolverから一つの子の関数が呼ばれる場合（ApplicationServiceレイヤーのメソッドなど）はそちらで `errorLogger` を呼ぶといった形です。

```typescript
const resolvers = {
  Query: {
    user(parent, args, context, info) {
      // もしくはApplicationServiceレイヤーのメソッドを呼び、そのメソッド内でtry/catchするなど
      try {
        return users.find(user => user.id === args.id);
      } catch (e) {
        errorLogger(e);
      }
    }
  }
}
```

若干話がそれますが、エラーを通知後、レスポンスとして返すエラーを [formatError](https://www.apollographql.com/docs/apollo-server/data/errors/#for-client-responses) 関数内で整形している場合、context（[公式ドキュメント](https://www.apollographql.com/docs/apollo-server/security/authentication/)によると認証処理等を行う場所、という認識）からthrowされるエラーは `ApolloError` のインスタンスでないとランタイムエラーとなってしまう現象がありました（resolverからthrowされるエラーは `GraphQLError` というクラスのインスタンスに変換された上で `formatError` に渡っているように見えます）。


### 2. ApolloServerの `didEncounterErrors` イベントを活用する
[Sentryの公式ブログ](https://blog.sentry.io/2020/07/22/handling-graphql-errors-using-sentry) でも紹介されているのですが、ApolloServerの `didEncounterErrors` イベントを活用しエラーを送信する、というやり方もあります。ただしこの場合だとエラーの内容を元に1箇所で処理を分岐することになってしまうかと（この場合はこういったエラーを送信する、もしくはこの場合はエラーを送信しないなど）。

個人的には1で良いかと思っています。


## フロントエンド

以下のような形のエラー通知が考えられます。

1. GraphQLのquery/mutationそれぞれのoperationごとの `error` レスポンスデータを確認してエラー通知
1. ApolloClientのインスタンス化の際に `onError` 関数を `errorLink` に渡す

### 1. GraphQLのquery/mutationそれぞれのoperationごとの `error` レスポンスデータを確認してエラー通知

```typescript
const { loading, error, data } = useQuery(GET_DOGS);
if (error) {
  // 実際にはもう少しerrorの中身を確認してから通知するか決める
  errorLogger(error);
}
```
GraphQLのquery/mutationそれぞれのoperationごとの `error` レスポンスデータを確認し、必要な際に都度 `errorLogger` 関数を呼びます。通知だけでなく、特定のエラーが発生した際に共通の関数を呼び、エラーハンドリングを行う、といった処理もここでできそうです。

### 2. ApolloClientのインスタンス化の際にonError関数をerrorLinkに渡す

```typescript
const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    // エラーの内容に応じて errorLoggerを呼ぶ
  }
}

const client = new ApolloClient({
  cache: new InMemoryCache()
  link: from([errorLink, httpLink]),
});
```

この `onError` 関数内で `errorLogger` を呼ぶ形なのですがいくつか懸念点があります。

1. ApolloServerからのレスポンスの内容（ `code` など）を元にonError関数内で処理を分岐することになり（この場合はこういったエラーを送信する、もしくはこの場合はエラーを送信しないなど）、分岐の処理がどんどん増えていく可能性がある
1. プロジェクトでSentryを使っているとこのonError関数でエラーが上手くcatchされず、本来に握りつぶしたかったエラーがSentryに送信されてしまう、という現象が過去に発生しました（[こちら](https://github.com/apollographql/apollo-client/issues/5708)が恐らく関連issue）

なので基本的に1の方法で良いかと思っています。そしてそもそもフロントエンドのチームがApolloServerまで管理している体制下でフロントエンドのGraphQLエラーとバックエンドのエラーの内容が重複する場合、フロントエンド側の通知は省いてしまっても良いかもしれません（ネットワークの問題でGraphQLサーバーと通信できなかった場合は除く。その際はバックエンド側から感知できないため）。

## まとめ

最近このエラー通知まわりで少し苦労したため、全体的な整理をしてみました。エラーの種類が将来的に増えても容易に個別対応ができる設計が良さそうです。