T-CREATOR

Vite 本番の可観測性:ソースマップアップロードと Sentry 連携でエラーを特定

Vite 本番の可観測性:ソースマップアップロードと Sentry 連携でエラーを特定

本番環境で発生したエラーを特定するのは、想像以上に困難です。Vite でビルドされたアプリケーションは、コードが圧縮・難読化されているため、エラースタックトレースを見ても元のコードの位置がわかりません。そこで重要になるのが、ソースマップと Sentry を活用した可観測性の確保ですね。

本記事では、Vite で構築したアプリケーションにおいて、本番環境のエラーを効率的に特定するための Sentry 連携とソースマップアップロードの実装方法を解説します。実際に動作する設定例とともに、エラー監視の仕組みを体系的に理解できる内容となっています。

背景

本番環境でのエラー特定の難しさ

Vite は高速なビルドツールとして人気ですが、本番ビルド時にはコードを最適化・圧縮します。その結果、ブラウザで発生したエラーのスタックトレースは以下のような読めない形式になってしまいます。

typescriptError: Cannot read property 'name' of undefined
  at r.createElement (main.a3f2b1c4.js:1:1234)
  at t (main.a3f2b1c4.js:2:5678)

開発環境では明確だったエラー位置が、本番環境では main.a3f2b1c4.js:1:1234 のような情報しか得られません。これでは、どのコンポーネントのどの行でエラーが発生したのか判断できませんね。

ソースマップの役割

ソースマップは、圧縮されたコードと元のソースコードをマッピングするファイルです。.js.map という拡張子を持ち、ビルドツールが自動生成します。

以下の図は、ソースマップを介してエラー位置を特定する流れを示しています。

mermaidflowchart LR
  browser["ブラウザ"] -->|圧縮エラー| sentry["Sentry"]
  sentry -->|ソースマップ参照| map["ソースマップ<br/>(*.js.map)"]
  map -->|元の位置特定| source["元のソースコード"]
  source -->|可読エラー| dev["開発者"]

ソースマップがあることで、UserProfile.tsx:42 のように、元のファイル名と行番号でエラーを表示できるようになります。エラーの原因特定が劇的に効率化されるでしょう。

Sentry の可観測性

Sentry は、エラー監視とパフォーマンストラッキングを提供する SaaS プラットフォームです。以下の機能により、本番環境の可観測性を向上させます。

#機能説明
1エラー収集ブラウザやサーバーで発生した全エラーを自動収集
2スタックトレース解析ソースマップを用いて元のコード位置を表示
3コンテキスト情報ユーザー情報、ブラウザ、OS などの環境情報を記録
4アラート通知重大なエラー発生時に Slack や Email で通知
5パフォーマンス監視ページ読み込み時間やトランザクション追跡

このように、Sentry を導入することで、本番環境での問題を迅速に発見し、対処できる体制が整います。

課題

セキュリティリスクとソースマップの管理

ソースマップを本番環境に公開すると、以下のようなセキュリティリスクが発生します。

  1. ソースコードの露出: ソースマップには元のコードがほぼそのまま含まれており、ビジネスロジックや API キーが漏洩する可能性があります
  2. 攻撃対象の拡大: 攻撃者がコード構造を把握することで、脆弱性を発見しやすくなります
  3. 知的財産の保護: 独自のアルゴリズムや実装詳細が競合他社に知られるリスクがあります

一方、ソースマップがないと以下の問題が発生します。

#課題影響
1エラー位置不明デバッグに数時間〜数日かかる
2再現困難ユーザー環境でのみ発生するエラーを特定できない
3パフォーマンス分析不可ボトルネックの特定が困難

このジレンマを解決するのが、Sentry へのプライベートなソースマップアップロードです。

以下の図は、課題の構造を示しています。

mermaidflowchart TB
  prod["本番環境"] --> choice{"ソースマップを<br/>公開する?"}
  choice -->|Yes| risk["セキュリティリスク<br/>・コード露出<br/>・攻撃対象拡大"]
  choice -->|No| debug["デバッグ困難<br/>・エラー位置不明<br/>・再現不可"]
  risk --> solution["解決策"]
  debug --> solution
  solution --> private["プライベートアップロード<br/>Sentry へのみ送信"]

公開するとリスク、公開しないとデバッグ困難という二律背反の状況を、Sentry へのプライベートアップロードで解消します。

ビルドプロセスへの統合

ソースマップアップロードを手動で行うのは、以下の理由から現実的ではありません。

  1. ビルドごとに変化: ハッシュ付きファイル名(main.a3f2b1c4.js)がビルドごとに変わるため、手動では追跡できません
  2. CI/CD との整合: 自動デプロイメントパイプラインに組み込まないと、アップロード漏れが発生します
  3. 複数環境: ステージング、本番など複数環境で異なるビルドを管理する必要があります

そのため、Vite のビルドプロセスに統合された自動アップロードの仕組みが不可欠ですね。

解決策

Vite プラグインによる自動化

Sentry 公式が提供する @sentry​/​vite-plugin を使用することで、ビルド時に自動的にソースマップをアップロードできます。このプラグインは以下の処理を自動で行います。

  1. ビルド後のソースマップファイルを検出
  2. Sentry API を使用してアップロード
  3. リリース情報(バージョン、コミットハッシュなど)を紐付け
  4. アップロード後、オプションでローカルのソースマップを削除

以下の図は、ビルドからアップロードまでの自動化フローを示しています。

mermaidflowchart LR
  build["vite build"] --> plugin["@sentry/vite-plugin"]
  plugin --> detect["ソースマップ検出"]
  detect --> upload["Sentry へアップロード"]
  upload --> associate["リリース情報紐付け"]
  associate --> cleanup["ローカル削除<br/>(オプション)"]
  cleanup --> dist["dist/ (*.js のみ)"]

このフローにより、開発者はビルドコマンドを実行するだけで、ソースマップ管理が完結します。手動作業が不要になり、ヒューマンエラーも防げますね。

環境変数による設定管理

本番ビルド時のみソースマップをアップロードするため、環境変数で制御します。以下の環境変数を .env.production に設定しましょう。

bash# Sentry 設定
VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
VITE_SENTRY_ORG=your-org-name
VITE_SENTRY_PROJECT=your-project-name
SENTRY_AUTH_TOKEN=your-auth-token

各環境変数の役割は以下の通りです。

#環境変数説明公開可否
1VITE_SENTRY_DSNSentry プロジェクトの公開エンドポイント○ クライアント側で使用
2VITE_SENTRY_ORGSentry 組織名○ アップロード用
3VITE_SENTRY_PROJECTSentry プロジェクト名○ アップロード用
4SENTRY_AUTH_TOKENAPI アクセストークン× 絶対に秘匿

SENTRY_AUTH_TOKEN は Sentry の設定画面から生成できます。このトークンには project:releases および org:read のスコープが必要ですね。

プライベートアップロードの仕組み

Sentry へのソースマップアップロードは、以下の特徴により安全性が保たれます。

  1. API 認証: Auth Token による認証が必須で、権限のないユーザーはアクセスできません
  2. プライベート保存: アップロードされたソースマップは Sentry 内部にのみ保存され、公開 URL は生成されません
  3. アクセス制御: Sentry プロジェクトのメンバーのみがソースマップを参照可能です
  4. 本番コードとの分離: 本番環境にデプロイされる dist​/​ フォルダには .js.map ファイルを含めません

この仕組みにより、エンドユーザーはソースマップにアクセスできず、開発チームのみが詳細なエラー情報を取得できます。

具体例

プロジェクトのセットアップ

まず、必要なパッケージをインストールします。Sentry の SDK とビルドプラグインの両方が必要です。

bashyarn add @sentry/react
yarn add -D @sentry/vite-plugin

@sentry​/​react はブラウザでのエラー捕捉を担当し、@sentry​/​vite-plugin はビルド時のソースマップアップロードを担当します。

Vite 設定ファイルの実装

次に、vite.config.ts でプラグインを設定しましょう。以下のコードは、環境変数を読み込み、条件付きでプラグインを有効化します。

typescriptimport { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
import { sentryVitePlugin } from '@sentry/vite-plugin';

export default defineConfig(({ mode }) => {
  // 環境変数の読み込み
  const env = loadEnv(mode, process.cwd(), '');

  return {
    plugins: [
      react(),
    ],
  };
});

環境変数を loadEnv で読み込むことで、.env.production の値にアクセスできるようになります。次に、プラグインの条件付き追加を実装しましょう。

typescriptexport default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '');

  // プラグインの配列
  const plugins = [react()];

  // 本番ビルド時のみ Sentry プラグインを追加
  if (mode === 'production' && env.SENTRY_AUTH_TOKEN) {
    plugins.push(
      sentryVitePlugin({
        org: env.VITE_SENTRY_ORG,
        project: env.VITE_SENTRY_PROJECT,
        authToken: env.SENTRY_AUTH_TOKEN,
      })
    );
  }

  return {
    plugins,
  };
});

この条件分岐により、開発環境ではプラグインが無効化され、ビルド時間が短縮されます。本番ビルド(mode === 'production')かつ認証トークンが設定されている場合のみ、ソースマップアップロードが実行されますね。

ソースマップ生成の設定

Vite にソースマップを生成させるため、build.sourcemap オプションを設定します。本番環境でのみ hidden モードを使用することで、セキュリティを保ちましょう。

typescriptexport default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '');
  const plugins = [react()];

  if (mode === 'production' && env.SENTRY_AUTH_TOKEN) {
    plugins.push(
      sentryVitePlugin({
        org: env.VITE_SENTRY_ORG,
        project: env.VITE_SENTRY_PROJECT,
        authToken: env.SENTRY_AUTH_TOKEN,
      })
    );
  }

  return {
    plugins,
    build: {
      // 本番では hidden、開発では true
      sourcemap: mode === 'production' ? 'hidden' : true,
    },
  };
});

sourcemap: 'hidden' の動作は以下の通りです。

#モード生成ファイルソースマップ参照セキュリティ
1true*.js, *.js.mapJS ファイルに ​/​​/​# sourceMappingURL=*.js.map 含む★ 低(公開される)
2'hidden'*.js, *.js.mapJS ファイルに参照コメントなし★★★ 高(公開されない)
3false*.js のみ生成されない★★★★ 最高(エラー追跡不可)

hidden モードでは、.js.map ファイルは生成されますが、ブラウザが読み込む JavaScript ファイルには参照が含まれません。そのため、ブラウザはソースマップを取得できず、Sentry のみが利用可能になります。

リリース情報の紐付け

ソースマップをリリースバージョンと紐付けることで、どのデプロイでエラーが発生したか特定できます。以下のコードで、Git のコミットハッシュをリリース名として使用しましょう。

typescriptimport { execSync } from 'child_process';

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '');
  const plugins = [react()];

  if (mode === 'production' && env.SENTRY_AUTH_TOKEN) {
    // Git コミットハッシュを取得
    const release = execSync('git rev-parse HEAD').toString().trim();

    plugins.push(
      sentryVitePlugin({
        org: env.VITE_SENTRY_ORG,
        project: env.VITE_SENTRY_PROJECT,
        authToken: env.SENTRY_AUTH_TOKEN,
        release: {
          name: release,
        },
      })
    );
  }

  return {
    plugins,
    build: {
      sourcemap: mode === 'production' ? 'hidden' : true,
    },
  };
});

release.name にコミットハッシュを設定することで、Sentry 上でエラーと特定のコミットを関連付けられます。デプロイ履歴との照合が簡単になりますね。

アップロード後の自動削除

セキュリティをさらに強化するため、アップロード後にローカルのソースマップファイルを削除できます。filesToDeleteAfterUpload オプションを使用しましょう。

typescriptif (mode === 'production' && env.SENTRY_AUTH_TOKEN) {
  const release = execSync('git rev-parse HEAD').toString().trim();

  plugins.push(
    sentryVitePlugin({
      org: env.VITE_SENTRY_ORG,
      project: env.VITE_SENTRY_PROJECT,
      authToken: env.SENTRY_AUTH_TOKEN,
      release: {
        name: release,
      },
      // アップロード後に削除するファイルパターン
      filesToDeleteAfterUpload: ['dist/**/*.map'],
    })
  );
}

このオプションにより、dist​/​ フォルダ内の .map ファイルがアップロード後に自動削除されます。デプロイパッケージにソースマップが含まれないため、誤って公開されるリスクがゼロになりますね。

ブラウザ側の Sentry 初期化

ビルド設定が完了したら、次はブラウザ側でエラーを捕捉する設定を行います。アプリケーションのエントリーポイント(通常は main.tsx または index.tsx)で Sentry を初期化しましょう。

typescriptimport React from 'react';
import ReactDOM from 'react-dom/client';
import * as Sentry from '@sentry/react';
import App from './App';

必要なモジュールをインポートします。@sentry​/​react は React 専用の統合機能を提供し、コンポーネントエラーも自動的に捕捉してくれます。

typescript// Sentry 初期化(本番環境のみ)
if (import.meta.env.PROD) {
  Sentry.init({
    dsn: import.meta.env.VITE_SENTRY_DSN,
    environment: import.meta.env.MODE,
    integrations: [
      Sentry.browserTracingIntegration(),
      Sentry.replayIntegration(),
    ],
  });
}

import.meta.env.PROD で本番環境のみ初期化することで、開発環境でのノイズを防ぎます。browserTracingIntegration はパフォーマンス監視を、replayIntegration はエラー発生時のユーザー操作を記録します。

typescript// React アプリケーションのマウント
ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

通常通り React アプリケーションをマウントします。Sentry の初期化は最初に実行されるため、アプリケーション全体のエラーを捕捉できますね。

リリースバージョンの同期

ビルド時に設定したリリースバージョンと、ブラウザ側の Sentry 初期化で使用するバージョンを一致させる必要があります。環境変数を使用して同期しましょう。

まず、vite.config.ts でリリースバージョンを環境変数として定義します。

typescriptimport { execSync } from 'child_process';

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '');
  const plugins = [react()];

  // リリースバージョンを取得
  const release = mode === 'production'
    ? execSync('git rev-parse HEAD').toString().trim()
    : 'dev';

  if (mode === 'production' && env.SENTRY_AUTH_TOKEN) {
    plugins.push(
      sentryVitePlugin({
        org: env.VITE_SENTRY_ORG,
        project: env.VITE_SENTRY_PROJECT,
        authToken: env.SENTRY_AUTH_TOKEN,
        release: {
          name: release,
        },
        filesToDeleteAfterUpload: ['dist/**/*.map'],
      })
    );
  }

  return {
    plugins,
    build: {
      sourcemap: mode === 'production' ? 'hidden' : true,
    },
    // リリースバージョンを環境変数として注入
    define: {
      'import.meta.env.VITE_RELEASE': JSON.stringify(release),
    },
  };
});

define オプションを使用することで、ビルド時に計算されたリリースバージョンをコード内で参照可能にします。

次に、ブラウザ側の初期化でこの値を使用します。

typescriptif (import.meta.env.PROD) {
  Sentry.init({
    dsn: import.meta.env.VITE_SENTRY_DSN,
    environment: import.meta.env.MODE,
    release: import.meta.env.VITE_RELEASE,
    integrations: [
      Sentry.browserTracingIntegration(),
      Sentry.replayIntegration(),
    ],
  });
}

これにより、Sentry がエラーを受信した際、正しいリリースのソースマップを使用してスタックトレースを復元できます。バージョンの不一致によるマッピングエラーが防げますね。

CI/CD パイプラインへの組み込み

GitHub Actions などの CI/CD 環境でビルドする場合、環境変数の設定が必要です。以下は GitHub Actions の設定例です。

yamlname: Build and Deploy

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3
        with:
          fetch-depth: 0  # Git 履歴を取得(コミットハッシュ用)

fetch-depth: 0 により、完全な Git 履歴を取得します。これがないと git rev-parse HEAD が正しく動作しません。

yaml      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: yarn install --frozen-lockfile

依存関係をインストールします。--frozen-lockfile により、yarn.lock の整合性を保証できます。

yaml      - name: Build application
        env:
          VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
          VITE_SENTRY_ORG: ${{ secrets.VITE_SENTRY_ORG }}
          VITE_SENTRY_PROJECT: ${{ secrets.VITE_SENTRY_PROJECT }}
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
        run: yarn build

GitHub Secrets に保存した環境変数を使用してビルドします。SENTRY_AUTH_TOKEN は絶対に直接記述せず、Secrets 経由で渡しましょう。

yaml      - name: Deploy to production
        run: |
          # デプロイコマンド(例: S3, Vercel, Netlify など)
          yarn deploy

ビルド成果物をデプロイします。この時点で dist​/​ フォルダには .map ファイルが含まれていないため、安全にデプロイできますね。

エラーの確認とデバッグ

デプロイ後、実際にエラーが発生したときの Sentry の動作を確認しましょう。以下のようなテストコードを本番環境で実行してみます(テスト後は削除してください)。

typescript// App.tsx
import { useEffect } from 'react';
import * as Sentry from '@sentry/react';

function App() {
  useEffect(() => {
    // テスト用エラー(本番確認後は削除)
    const testError = () => {
      throw new Error('Sentry テストエラー: ソースマップ確認用');
    };

    // 5秒後にエラーを発生させる
    setTimeout(testError, 5000);
  }, []);

  return <div>アプリケーション</div>;
}

export default App;

このコードをデプロイして 5 秒待つと、Sentry ダッシュボードにエラーが表示されます。ソースマップが正しくアップロードされていれば、以下のような情報が確認できるでしょう。

#項目正常時の表示異常時の表示
1ファイル名App.tsxmain.a3f2b1c4.js
2行番号8 (実際の行)1 (圧縮後の行)
3関数名testErrorr (圧縮後の名前)
4コード表示元のコードが表示される圧縮コードが表示される

正常時には、元のソースコードが Sentry 上で確認でき、エラーの原因を即座に特定できます。

トラブルシューティング

ソースマップが正しく適用されない場合、以下の点を確認してください。

Error: Release not found

bashError: Release 'abc123...' not found in Sentry

このエラーは、ブラウザ側で指定したリリースバージョンとアップロードされたバージョンが一致しない場合に発生します。

解決方法:

  1. vite.config.tsdefine で定義したリリースバージョンを確認
  2. Sentry ダッシュボードの「Releases」ページでアップロード済みバージョンを確認
  3. git rev-parse HEAD の実行結果が CI/CD 環境とローカルで一致するか確認

Error: Source map not found

bashError: Source map for 'main.a3f2b1c4.js' not found

ソースマップファイルがアップロードされていない、またはファイル名が一致していない場合に発生します。

解決方法:

  1. build.sourcemap'hidden' または true に設定されているか確認
  2. ビルドログで @sentry​/​vite-plugin のアップロード成功メッセージを確認
  3. SENTRY_AUTH_TOKEN が正しく設定され、権限があるか確認

Error: SENTRY_AUTH_TOKEN is not set

bashError: SENTRY_AUTH_TOKEN environment variable is not set

認証トークンが設定されていない場合に発生します。

解決方法:

  1. .env.production ファイルに SENTRY_AUTH_TOKEN=... が記載されているか確認
  2. CI/CD 環境の Secrets 設定を確認
  3. トークンのスコープに project:releasesorg:read が含まれているか確認

以下の図は、トラブルシューティングのフローを示しています。

mermaidflowchart TD
  error["エラー発生"] --> check_release{"リリースバージョン<br/>一致?"}
  check_release -->|No| fix_release["define 設定確認<br/>Git ハッシュ確認"]
  check_release -->|Yes| check_map{"ソースマップ<br/>アップロード済み?"}
  check_map -->|No| fix_upload["sourcemap 設定確認<br/>Auth Token 確認"]
  check_map -->|Yes| check_token{"Auth Token<br/>有効?"}
  check_token -->|No| fix_token["Token 再生成<br/>スコープ確認"]
  check_token -->|Yes| success["正常動作"]

段階的に原因を切り分けることで、問題を効率的に解決できるでしょう。

まとめ

本記事では、Vite で構築したアプリケーションにおける本番環境の可観測性向上について、Sentry 連携とソースマップアップロードの実装方法を解説しました。

重要なポイントをまとめます。

  1. ソースマップの必要性: 本番環境の圧縮コードでは、エラー位置の特定が困難です。ソースマップがあることで、元のコードと紐付けてデバッグできます
  2. セキュリティとのバランス: sourcemap: 'hidden' により、ソースマップを生成しつつブラウザからはアクセス不可にできます。Sentry へのプライベートアップロードで、セキュリティとデバッグ効率を両立させましょう
  3. 自動化の重要性: @sentry​/​vite-plugin を使用することで、ビルドプロセスに統合された自動アップロードが実現します。手動作業によるミスを防げますね
  4. リリース管理: Git コミットハッシュをリリースバージョンとして使用することで、エラーと特定のデプロイを関連付けられます。デプロイ履歴との照合が容易になります
  5. CI/CD 統合: GitHub Actions などの CI/CD 環境で環境変数を適切に設定することで、自動ビルド・デプロイパイプラインに組み込めます

これらの仕組みを導入することで、本番環境で発生したエラーを迅速に特定し、ユーザー体験の向上とシステムの信頼性確保につながるでしょう。エラー監視は、サービスの品質を左右する重要な要素です。

関連リンク