Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

Source Mapを使ってフロントエンドのログを安全に効率よく扱う

ログ収集の重要性

近年、サイバー攻撃の増加に伴い、ログ収集と監視はシステムの防御戦略として不可欠です。特に、攻撃の予兆検知やインシデント対応などの観点からログ管理はセキュリティ強化の鍵を握っています。

またビジネス観点からも、ユーザーの行動分析や使用状況に合わせたサービス改善など、ログを適切に収集することの重要性が高まっています。

この記事ではフロントエンドのログ収集をより効率的なものにする方法を提案します。以下のような方々を主な対象読者としています。

  • 主にフロントエンドの開発をされている方
  • ログについて興味のある方
  • 何かしらのサービスを用いてすでにログ収集を実施している方
  • フロントエンドのログ収集方法に悩んでいる方

フロントエンド特有の問題

フロントエンドのログ収集は、バックエンドと異なりユーザーのブラウザ環境やクライアントサイドでの挙動に依存するため、特有の問題を抱えています。特に以下のような課題が挙げられます。

Source Mapを配布できない

フロントエンドのランタイムコードは、通常ビルドプロセス(webpack, Vite, esbuild など)によって minify(圧縮・難読化)されます。その結果、ランタイムエラーの発生箇所がオリジナルコードと一致しないため、エラーの調査が困難になります。

例えば以下のような場合があります。

// 開発時のコード
function fetchUserData(userId) {
    return fetch(`/api/users/${userId}`)
        .then(response => response.json())
        .catch(error => console.error("Failed to fetch user data", error));
}

// Minify 後のコード
function a(b){return fetch(`/api/users/${b}`).then(c=>c.json()).catch(d=>console.error("Failed to fetch user data",d));}

エラーログに a is not defined というメッセージが出ても、どの関数が問題なのか判別が難しくなります。

通常このような場合、Source Mapによりランタイムコードとオリジナルコードを照らし合わせることで、問題の発見が容易になります。しかしフロントエンドの場合、Source MapをWeb上に公開することは理想的ではありません。

Source Mapがあればオリジナルコードを復元できる可能性があり、その結果、会社の資産が流出するおそれがあります。 またそれによりシステムの弱点や脆弱性を発見されやすくなる可能性があります。したがって、Source Mapについては安全に取り扱う必要があります。

Source Mapとは何か

Source Mapは、ブラウザ上で実行されるminifyされたJavaScriptコードを、人間が読めるオリジナルのソースコードに対応付けるためのマッピングファイルです。

フロントエンドのコードは、本番環境でのパフォーマンスを最適化するために以下のような変換がされます。

  • 難読化(Obfuscation): 変数や関数名が短縮される(例:fetchUserData()a()
  • 圧縮(Minification): 余分なスペースや改行が削除される
  • バンドル(Bundling): 複数のモジュールを1つのファイルに統合する

この結果、本番環境でのエラーログは以下のように読みにくい形になります。

TypeError: a is not a function at bundle.js:1:12345

Source Mapを使用すると、これをオリジナルのコードと対応付け、デバッグやログ解析がしやすくなります。

デバッグやログ解析での役割

Source Mapは、以下の用途で活用されます。

開発者ツールでのデバッグ

ブラウザの開発者ツール(DevTools)がSource Mapを読み込むと、オリジナルのソースコードを表示できます。例えば、console.log() の出力や debugger の停止位置が元のコードの行番号で確認できるようになります。

エラーログの解析

本番環境で発生したエラーのスタックトレース(stack trace)は、難読化されたコードの行番号を示します。Source Mapを使うことで、実際のコードのどこでエラーが発生したのか特定できるようになります。

Source Mapなし

TypeError: Cannot read properties of undefined (reading 'map')
    at a (bundle.js:1:5678)

Source Mapあり

TypeError: Cannot read properties of undefined (reading 'map')
    at fetchUserData (src/utils/api.js:42:10)

ブラウザ拡張機能によるノイズ

もう1つの問題として、ユーザーがインストールしているブラウザ拡張機能がログにノイズを混入させることがあります。

これにより、アプリケーションとは無関係なエラーが大量に記録され、ログの可読性が低下します。

例えば以下はブラウザの拡張機能がchrome APIを使用しようとして発生するエラーですが、アプリ側とは関係ありません。

Uncaught ReferenceError: chrome is not defined
    at <anonymous>:1:1

改善案

今回実施したプロジェクトの主な技術スタックはこのような感じです。 なおログ管理ツールとしてRollbarを使用しましたが、おそらく他のツールでも同じことが実現できると思います。

  • Rollbar (ログ管理)
  • Next.js (フロントエンドフレームワーク)
  • CircleCI (CI)

上記で説明してきた制約の中でより安全で効率的なログ収集を行うために、以下のような仕組みを導入しました。

Source Mapを活用したログ収集のシーケンス図

それぞれのステップについて説明していきます。

CIからSource Mapをアップロード

まずデプロイのCIワークフローの中でSource Mapを生成します。

Next.jsの場合、以下の設定をすれば本番環境のSource Mapを生成することができます。開発環境に関してはデフォルトで生成されるようになっているので特に設定は必要ありません。

module.exports = {
  productionBrowserSourceMaps: true,
}

そして生成されたSource MapをRollbarへアップロードしますが、このときどのビルドバージョンのSource Mapであるかを明示する必要があります。

これはRollbar内ではcode_versionとして管理されます。今回はビルドバージョンにCIRCLE_SHA1を使用しました。

circleci.com

ビルド成果物を配信

次に、通常のデプロイと同じようにAWSなどを用いてビルド成果物を配信します。この際の注意点として、ビルドされたJSだけでなく、すでにRollbarへアップロード済みのSource Mapも配信する必要があります。

この理由は先ほどのSource Mapアップロード時、ファイルの実体をアップロードしているのではなく、あくまでSource Mapへのパスを共有しているだけだからです。

例えば./.next/static/chunks/main.jsに対応するSource Mapが./.next/static/chunks/main.js.mapであることを明示するようなイメージです。

こうすることでランタイムでmain.jsからエラーが発生した場合、main.js.mapを参照すれば良いということをRollbarに伝えることができます。

RollbarにSource Mapの実体がアップロードされない挙動については筆者が観測しただけでアップロード方法が間違っている可能性があります。
ご存知の方がいらっしゃいましたらコメントしていただけると嬉しいです。

Source Mapリクエスト

ブラウザからRollbarへエラー情報が送られると、Rollbarは先ほどアップロードした情報を元にNext.jsサーバーへSource Mapをリクエストします。

Next.jsサーバーはSource MapへのリクエストがRollbarのものからであることを確認し、Source Mapを返します。RollbarのIPについては以下を参照しています。

Rollbar IP Addresses

このときSource Mapの取得に失敗すると以下のようなエラーログが表示されます。例ではSource Mapのパスが指定されていない際のエラーです。

Source Mapのダウンロードに失敗している画像

エラー解析

RollbarはSource Mapを取得するとビルドされたJSと突き合わせ、エラーの解析を行います。

Rollbarへ適切にSource Mapが共有されているかは設定タブのSource Mapsから確認することができます。

Source Mapの設定タブ

実装

上記のいくつかのセクションについて、サンプルコードを元に詳細な実装を紹介していきます。

CIからのアップロード

- run:
    name: Setup Rollbar code version
    command: |
      # Set CIRCLE_SHA1 as the code_version for both the SourceMap and Client.
      echo 'export ROLLBAR_CODE_VERSION="$CIRCLE_SHA1"' >> "$BASH_ENV"

Setup Rollbar code versionではRollbarで使用されるcode versionを環境変数にしています。


- run:
    name: Install yq
    command: |
      wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O yq
      chmod +x yq

Install yqではyqをインストールし、権限を与えています。

yqはyamlファイルを操作するために使用しています。他の方法でも代替できるので無くてもよいです。


- run:
   name: Upload SourceMap to Rollbar
    command: |
      export APP_HOST=$(yq eval '.APP_HOST' ./workspace/.circleci/config/<< parameters.deploy_env >>.yml)

      for path in $(find ./workspace/.next -name "*.js" -or -name "*.css"); do
          if [ -e "$path.map" ]; then
          file=${path/.next/_next}
          url=${APP_HOST}/$file
          minified_url=$(echo "$url" | sed 's|./workspace/||')
          source_map="@$path.map"

          curl --show-error https://api.rollbar.com/api/1/sourcemap \
              -F access_token=${ROLLBAR_TOKEN_DEV} \
              -F version=${CIRCLE_SHA1} \
              -F minified_url=$minified_url \
              -F source_map=$source_map
          fi
      done

Upload SourceMap to Rollbar は実際にSource MapをRollbarへアップロードする処理です。

ここで./workspace/が出てきますが、これはビルド成果物を有効活用するためのCircleCIのワークスペースです。

./workspace/が共有しているワークスペースになります。

ワークスペースによるジョブ間のデータ共有 - CircleCI

.circleci/config/stg.ymlに以下のようにAPP_HOSTを定義し、Source MapのURLを環境ごとに変更しています。

アップロードの詳細については3. Upload your source map file(s)にあるのでaccess_tokenなどはこれを参考に設定いただければと思います。

APP_HOST: https://example.com

全体的には以下のような流れになります。

実際に使用する時はこのjob以前にnpm run buildなどによりSource Mapが生成されていることに注意してください。

- run:
    name: Setup Rollbar code version
    command: |
    # Set CIRCLE_SHA1 as the code_version for both the SourceMap and Client.
    echo 'export ROLLBAR_CODE_VERSION="$CIRCLE_SHA1"' >> "$BASH_ENV"
- run:
    name: Install yq
    command: |
    wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O yq
    chmod +x yq
- run:
    name: Upload SourceMap to Rollbar
    command: |
    export APP_HOST=$(yq eval '.APP_HOST' ./workspace/.circleci/config/<< parameters.deploy_env >>.yml)

    for path in $(find ./workspace/.next -name "*.js" -or -name "*.css"); do
        if [ -e "$path.map" ]; then
        file=${path/.next/_next}
        url=${APP_HOST}/$file
        minified_url=$(echo "$url" | sed 's|./workspace/||')
        source_map="@$path.map"

        curl --show-error https://api.rollbar.com/api/1/sourcemap \
            -F access_token=${ROLLBAR_TOKEN_DEV} \
            -F version=${CIRCLE_SHA1} \
            -F minified_url=$minified_url \
            -F source_map=$source_map
        fi
    done

Source Mapのリクエストハンドリング

先ほどのシーケンス図のSource Mapリクエストに関して以下のハンドリングを行っています。
ハンドリングする理由として、Source Mapをインターネットに公開すると、ビルドされたソースコードから元のソースコードを復元できてしまう可能性があるからです。

Source MapへのリクエストはRollbarからのみ受け付けるよう設定することで対策を行いました。
この対策方法はインフラ構成にもよりますが、今回はAWS ALBで*.js.mapへの全てのリクエストを404へリダイレクトしています。

Source Mapをインターネットに公開すると、ビルドされたソースコードから元のソースコードを復元されてしまう可能性があります。そのためSource MapへのリクエストはRollbarからのみ受け付けるように設定します。

インフラ構成にもよりますが、今回はAWS ALBで*.js.mapへの全てのリクエストを404へリダイレクトしています。

導入後の変化

最後にSource MapをRollbarなどログ収集サービスへアップロードすることで得られるメリットを紹介します。

1. エラー発生箇所がビルド前のコードでわかる

Source Mapがない状態であっても、ランタイムで動いているJSの情報からどんなエラーが発生したのかをできる限り表示してくれます。
しかし、ビルド前のソースコードの情報が得られないため、これでは/src/appあたりでエラーが発生したことくらいしかわからず、エラーメッセージから「ユーザーがどんなユースケースでエラーに遭遇したのか」を推測しなければなりません。

Stack Trace

しかしSource Mapが適切にアップロードされていれば元のソースコードの情報を詳細に知ることができ、どんな状況でエラーが起きたのかを調べやすくなります。

Stack Traceの詳細

2. ノイズが減る

フロントエンドではソースコード起因ではないエラーを観測することがあるため、エラーログ監視作業に余分なコストが発生することがよくあります。

しかしSource Mapがあると以下のようにバンドラーでハンドルしたプロジェクトのJS以外で発生したエラーについてはnon-project framesになり、デフォルトではたたまれて表示されるようです。

non-project frames

このフィルタリング機能によって、我々開発者はプロダクトで発生したエラーに絞って調査することができるようになります。

まとめ

以上、Source Mapの導入方法について説明してきました。

ログのノイズが多い問題に直面している方にとって、この記事が良い提案となれば幸いです。

実際のところ、まだ運用には至っていないため、これから実際に運用してみるとうまくいかないケースも出てくるかもしれません。しかしそういったケースにも臨機応変に対応していき、さらにログを効率よく有意義なものにしていきたいと思っています。
また知見が得られたらブログを書くかもしれないので、ご期待ください!

ここまで読んでくださりありがとうございました。

参考資料

docs.rollbar.com

syossan.hateblo.jp

nextjs.org

docs.rollbar.com