フロントエンドのログ周りを改善する過程で OSS (Datadog Browser SDK) にコントリビュートした話

![READYFORアドベントカレンダー2021カバー画像.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83038/baab6a51-4f24-ce83-210a-4795bed49d96.jpeg)

この記事は [READYFOR Advent Calendar 2021](https://qiita.com/advent-calendar/2021/readyfor)の21日目の記事です。

## はじめに

こんにちは。READYFOR でフロントエンドエンジニアとして働いている菅原（[@kotarella1110](https://twitter.com/kotarella1110)）です！
私はシステム基盤部に所属し、フロントエンド基盤 sqd というスクワッドでフロントエンドの横断的な課題達成や生産性・品質向上に繋がるような施策に取り組んでいます。
現在その取り組みの一環としてフロントエンドのログ周りを改善しており、その過程で Datadog Browser SDK という OSS にコントリビュートすることができました。
OSS へのコントリビュートと聞くとなんとなく敷居が高いイメージを持つ方がいるかもしれませんが、実際のところそんなことはありません。

本記事ではこれについてのお話ができればと思っております。

## フロントエンドのログ周りの改善について

READYFOR では [Datadog](https://www.datadoghq.com/ja/) を導入してサービスの監視やログ収集を行なっています。
もちろんフロント側もブラウザログを収集するために [Datadog Logs](https://docs.datadoghq.com/ja/logs/) の導入はしていますが、以下のような問題がありあまり活用ができていない状況でした。

- Datadog を活用したログ管理の運用方針の整備がされていない
- ランタイムエラーではないが、ログを送信すべき箇所でログを送信できていない
- ネットワーク Abort エラーや Chrome 拡張のエラーといった対応不要なエラーログが大量に送信されている
- 対応不要なエラーログが大量にあるため、確認が必要なログが埋もれてしまっている
- 対応不要なエラーログが大量にあるため、Slack への通知設定を有効にできない
- フロント側で障害が発生していても気付きにくい

これらの問題を解決するのが私の現在の取り組みです。

## Datadog Logs を活用したブラウザログ収集について

[公式ドキュメント](https://docs.datadoghq.com/ja/logs/log_collection/javascript/)にも記載がありますが、以下のように [`@datadog/browser-log`](https://www.npmjs.com/package/@datadog/browser-logs) を npm でインストールし初期化するだけでブラウザログの収集ができます。
`@datadog/browser-log` は [Datadog Browser SDK](https://github.com/DataDog/browser-sdk) に OSS としてモノレポで管理されています。

```sh
npm i @datadog/browser-logs
```

```ts
import { datadogLogs } from '@datadog/browser-logs'

datadogLogs.init({
  clientToken: '<DATADOG_CLIENT_TOKEN>',
  site: '<DATADOG_SITE>',
  forwardErrorsToLogs: true,
  sampleRate: 100,
})
```

`forwardErrorsToLogs` オプションを `true` に設定すると、Browser SDK がエラーをトラッキングし、発生したエラーを自動で Datadog に送信してくれます（デフォルト `true`）。
Browser SDK によって送信されるエラーは以下の通りです。

| 送信されるログ | `error.origin` | 説明 |
|-|-|-|
| コンソールエラー | `console` | `console.error` のログが Datadog に送信されます。 |
| ランタイムエラー | `source` | ランタイムエラーが Datadog に送信されます。 `window` の `error` イベント及び `unhandledrejection` イベントでエラーをハンドリングしています。 |
| ネットワークエラー | `network` | Fetch 及び XHR のネットワークエラーが Datadog に送信されます。[ステータスが 0 （ネットワーク Abort）または 500 以上](https://github.com/DataDog/browser-sdk/blob/v3.10.0/packages/logs/src/domain/trackNetworkError.ts#L49-L55)の時にネットワークエラーと判断されます。 |

また、Datadog にはログレベルが存在しブラウザログレベルは以下の4段階あります。
`forwardErrorsToLogs` で送信されるエラーのログレベルは `error` です。

- `error`
- `warn`
- `info`
- `debug`

[Datadog Logs のエクスプローラー](https://docs.datadoghq.com/ja/logs/explorer/)上でログレベルによるフィルタリングが可能なため、ログレベル毎に要件を定め、送信する（もしくは Browser SDK によって送信される）ログのログレベルを適切に設定することが管理のしやすさに繋がります。

## Browser SDK のバグと思われる事象との遭遇

ログ周りの改善の一つとして「`forwardErrorsToLogs` で送信されるネットワークエラーのログレベルを `error` から `warn` に変更する」という対応を行いました。
ネットワークエラーはバックエンド側で早急な対応が必要になったとしても、フロントエンド側での対応は不要なケースがほとんどです。READYFOR ではブラウザログレベルの `error` 要件を「サービスに影響を与える可能性のある予期しないランタイムエラーで早急な対応が必要、全てのログを通知する」としており、ネットワークエラーはこの要件に合致しません。  そのため、ログレベルを `error` から `warn` に変更するという判断に至りました。
送信されるログのログレベルを変更したい場合は `beforeSend` コールバック関数を使用できます。`beforeSend` は Datadog に送信される前に Browser SDK によって収集された各ログにアクセスすることが可能で、任意のプロパティを更新したりログを破棄することができます。

```ts
datadogLogs.init({
    ...,
    forwardErrorsToLogs: true,
    beforeSend: (log) => {
      // ネットワークエラーのログのみ
      if (log.error?.origin === "network") {
        // ログレベルを warn に変更する
        log.status = "warn";
      }
    },
    ...
});
```

しかし、自分がこのコードを記述した時点の Browser SDK のバージョン [v3.8.0](https://github.com/DataDog/browser-sdk/tree/v3.8.0) では以下のような型エラーが発生してしまいました。これは、`beforeSend` の引数で受け取れる [`LogsEvent`](https://github.com/DataDog/browser-sdk/blob/v3.8.0/packages/logs/src/logsEvent.types.ts) の `status` プロパティが `readonly` だったためです。

```ts
datadogLogs.init({
    ...,
    forwardErrorsToLogs: true,
    beforeSend: (log) => {
      if (log.error?.origin === "network") {
        log.status = "warn"; // error TS2540: Cannot assign to 'status' because it is a constant or a read-only property.
      }
    },
    ...
});
```

一旦 `@ts-ignore` でこの型エラーを無視して動作確認をしてみたところ、期待通りネットワークエラーのログレベルが `warn` に変更されていました。バグかな？とも思いましたが、特定のプロパティのみ `readonly` にしているということはユーザー側での更新を避けたい事情が Datadog 側であるかもしれません。
そのため、PR を作成する前に一旦 `status` プロパティの更新が許可されているかどうか Issue で確認してみました。

## Browser SDK へのコントリビュート

こちらが作成した Issue です。

https://github.com/DataDog/browser-sdk/issues/1195

回答としては型に問題がありバグとのことだったので PR をすぐに作成しました。

https://github.com/DataDog/browser-sdk/pull/1198

ただ、以下のような発生日時やユニークな ID といった `readonly` プロパティについてはユーザー側からの更新を避けたいはずです。

- `date`
- `application_id`
- `service`
- `session_id`
- `view.id`

これらのプロパティについては `readonly` を削除すべきでは無いのでは？というやり取りを途中したりしましたが、JS で特に制限を設けておらず TS のみで制限を設ける必要はないという理由から、最終的には `LogsEvent` から全ての `readonly` を削除することになりました。

数日後、この PR はマージされリリースされました 🎉

https://github.com/DataDog/browser-sdk/blob/main/CHANGELOG.md#v390

> ## v3.9.0
>
> - 🐛 remove readonly from all LogsEvent properties ([#1198](https://github.com/DataDog/browser-sdk/pull/1198))

やはり軽微な修正でも普段お世話になっている OSS のリリースに含まれると嬉しいものですね！

## 今後

現在も引き続きログ周りを改善中です。
ログレベルの変更の対応以外にも、「`forwardErrorsToLogs` で送信されるコンソールエラーを `beforeSend` を使用して送信しないようにする」といった対応を行なったりしました。

```ts
datadogLogs.init({
    ...,
    forwardErrorsToLogs: true,
    beforeSend: (log) => {
      if (log.error?.origin === "console") {
        return false;
      }
    },
    ...
});
```

私はこれを `beforeSend` を使用して制御するのではなく、`forwardErrorsToLogs` で送信されるログをオプションで選択して制御できるとより便利だと思いました。
これは一般的なユースケースだと感じ、最近以下のような機能提案の Issue と PR を作成しています。

https://github.com/DataDog/browser-sdk/issues/1202

https://github.com/DataDog/browser-sdk/pull/1205

翌四半期にオプション追加に伴う見直しがあるようで、この機能提案がすぐに廃止されてしまうのを避けるためにクローズされてしまいましたが、READYFOR フロントエンドのログ周りの改善と併せて引き続き Browser SDK への機能提案・コントリビュートもしていければと思っております。

~~また、Datadog Logs は現状 [Sentry にあるようなソースマップの統合](https://docs.sentry.io/platforms/javascript/sourcemaps/) をサポートしていないため、トランスパイルされたコードのデバッグが非常にしにくいです。ソースマップファイルを本番に配置することで解決はしますが、トランスパイル前のコードが公開されてしまうことに抵抗があります。
※ [Datadog RUM では Source Map の統合をサポート](https://docs.datadoghq.com/ja/real_user_monitoring/guide/upload-javascript-source-maps/?tab=webpackjs)しています。~~

https://github.com/DataDog/browser-sdk/issues/284

~~これに関しては Datadog Logs のエクスプローラー側の修正も必要になってくるため、[datadog-ci](https://github.com/DataDog/datadog-ci) へのコントリビュートでどうこうなる話ではありません。
そのため、ソースマップをアップロードしておけば対応するリリースに紐づくソースマップを Datadog Logs のエクスプローラー上で表示してくれるような Chrome 拡張を作ってみるのも面白いかもなと思っています。~~

直接 Datadog のサポートに問い合わせてみたところ、いつからかは不明ですが Datadog Logs もソースマップの統合をサポートしていたことが分かりました。 datadog-ci でソースマップをアップロードしてみたところエクスプローラー上でビルド前のファイル名とエラー行数が表示されるようになりました 🎉

## 最後に

OSS へのコントリビュートに対して敷居が高いというイメージを持っていた方は、本内容を通してそのイメージがだいぶ払拭されたのではないでしょうか？
実際今回のコントリビュートは誰にでもできる型の軽微な修正です。このようなコントリビュートでもコミュニティへの貢献に繋がるのは事実です。
開発の過程で OSS のバグに遭遇したり、こんな機能があればいいのにな〜と感じたことが経験上あると思います。そんな時は、普段お世話になっている OSS に貢献するチャンスと捉えて積極的に Issue や PR を作成することをお勧めします！
コントリビュートを通して学べることは以下のように様々です。

- ドキュメントに記載されていないような仕様把握
- 普段使用しないような API や OSS が依存するライブラリ・テストツールの理解や使い方
- デザインパターンやテクニック
- テストの手法

今回は OSS へのコントリビュートにフォーカスした内容でした。
次回は Datadog Logs を活用したログ周りの改善にフォーカスした内容を投稿できればと思います。

それでは！👋

## アドベントカレンダー、明日の記事は

明日は [@shmokmt](https://qiita.com/shmokmt) さんの担当です！お楽しみに 🙌