QAフェーズへの移行を機に、後回しにしていたテスト基盤を一気に整備した記録です。
同じドメインで連携する3つのNext.jsサイト(利用者向け/提供者向け/社内管理画面)を並行開発し、リリース前のQAに突入したものの、モンキーテストで予想を超えるバグが多発しました。
修正のたびに3サイト全画面を手動で再確認することになり、1サイクルあたり30分〜1時間かかりました。本来テストシナリオ作成に充てるはずだった1週間がバグ修正で埋まりました。
そこでE2Eテスト(Playwright)・コンポーネントテスト(Vitest)・本番エラー監視(Sentry)を 半日で一気に導入 しました。
この記事では、Next.js App Router + TanStack Query + MSW という構成で、ツール選定から実装・CI統合までの過程を解説します。あわせて、SSR / Turbopack固有のハマりポイントと、AIでテストを量産する際の落とし穴も共有します。
この記事で得られること
- Next.js App Routerで Playwright + MSW を動かすためのSSR対応パターン
- MSW ハンドラーを E2E と Vitest で共有する設計(モックの二重管理を防ぐ)
- E2E / Vitest / 手動テストの 仕分け基準(テストピラミッドの設計)
- Sentryを Next.js 16 + Turbopack で動かす際のハマりポイントと解決策
- Sentryの 3層防御マスキング設計(SDKフック / Server-side Scrubber / Generative AI機能停止)
- ソースマップを Sentry にアップロードしない という運用選択肢とそのトレードオフ
- AIでテストを量産する際の「成功するテストしか作らない」問題と対策
動作確認環境
| 種別 | バージョン |
|---|---|
| OS | macOS 15.x |
| Node.js | 24.x |
| パッケージマネージャー | pnpm 10.x |
| Next.js | 16.x(Turbopack) |
| React | 19.x |
| @playwright/test | 1.58.x |
| vitest | 4.x |
| msw | 2.x |
| @sentry/nextjs | 10.x |
| @tanstack/react-query | 5.x |
1. 背景: 手動テストだけでは回らなくなった瞬間
3つのNext.jsサイト(利用者向けサイト/提供者向けサイト/社内管理画面)を約10名のチームで半年かけて並行開発しました。
開発スピードを優先する判断から、フロントエンドの自動テストは整備されないままQAフェーズに突入しました。
具体的に何が起きていたか:
- モンキーテスト初日に予想を超えるバグが多発 — テストシナリオ作成予定の1週間がバグ修正で埋まる
- バグ修正のたびに全画面を手動で再確認(1回30分〜1時間)→ 修正→デグレ→再修正のサイクル
- 3サイトが共通の API を使っているため、1サイトの修正が他サイトに影響していないかの確認が困難
- テスト手順が担当者の頭の中にしかなく、品質保証が完全に属人化
- AI(Claude Code / Devin等)でコードを生成しているものの、設計品質のチェック不在で手戻りが多発
開発は完了しているのにリリースできない——その最大の理由が「品質保証の仕組みがない」ことでした。
なぜ QA フェーズで腰を据えてテスト戦略を設計するのか
本来、テスト戦略は開発初期に設計すべきです。今回のプロジェクトでは、フロントエンドの開発スピードを優先する判断から、自動テストの整備は後ろ倒しになっていました。
QAフェーズに入り、手動テストの限界が明らかになったこのタイミングで、「とりあえずテストを書く」のではなく チームの標準となるテスト戦略を設計する 判断をしました。
今回設計するテスト戦略は、現行プロジェクトの品質保証だけでなく、次期プロジェクトで開発初日からテストが回る体制を作るための基盤でもあります。
2. ツール選定: なぜ Playwright × Vitest × Sentry なのか
E2E: Playwright を選んだ決定的な理由
E2Eツールは6つの候補(Playwright / Cypress / WebdriverIO / Nightwatch / TestCafe / CodeceptJS)を比較しました。結論は Playwright 一択 です。
一次選考: Playwright / Cypress 以外を落とした理由
3サイト横断のテスト基盤としてAI連携・SSR対応・運用実績の3軸で評価しました。
| ツール | 落とした理由 |
|---|---|
| WebdriverIO | Selenium ベースで起動・実行が重く、CI 時間が3サイト並列で許容外。MSW 連携の知見も少ない |
| Nightwatch | コミュニティ規模が小さく、Next.js App Router × SSR の事例が見つからない。社内に知見保有者なし |
| TestCafe | 開発ペースが鈍化し、最新の Next.js / React 19 環境での動作実績が乏しい |
| CodeceptJS | ラッパー型で内部は Playwright / WebdriverIO のいずれかに依存。直接 Playwright を採用するほうが抽象化のオーバーヘッドがない |
この時点で Playwright と Cypress の2択 に絞り込みました。
二次選考: Playwright が Cypress を上回った決定打
決め手は AI 連携・並列実行のスケーラビリティ・マルチブラウザ対応 の3点です。
| 比較軸 | Playwright | Cypress |
|---|---|---|
| AI 連携 | ◎ MCP Server + Codegen | ○ AI 機能(開発中) |
| 並列実行 | ◎ 標準で無料(fullyParallel: true) | △ 公式 --parallel は Cloud 必須。CI matrix + cypress-split で自前並列は無料可 |
| マルチブラウザ | ◎ Chromium/Firefox/WebKit | △ Chromium 中心 |
| デバッグ | ○ Trace Viewer + UI mode | ◎ タイムトラベル(直感的) |
| SSR + MSW 連携 | ◎ msw/node をアプリ側で起動 | ◎ 同上(ランナー非依存) |
並列実行についての補足です。Cypress Cloudが必須なのは公式の --parallel フラグそのものとload-balancing/insight機能だけです。spec分割をCI matrixで自前管理すれば無料でも並列化は可能です(cypress-split やspecファイル分割など)。今回は3サイト横断のメンテコストを抑える目的で、追加実装なしで並列化できるPlaywrightを優先しました。
3サイト横断のテスト基盤として、Playwrightを採用しました。決め手は次の3点です。第一に、社内で実証済みのAIエージェント連携(Playwright MCP / Codegen)。第二に、追加課金なしでスケールする並列実行。第三に、Chromium/Firefox/WebKitを網羅するマルチブラウザ対応です。Cypressのタイムトラベルデバッグは魅力的でしたが、上記3点の優位を覆すには至りませんでした。
SSR + MSW は「ランナーの違い」ではなく「msw/node をどこで起動するか」の問題
注意点として、prefetchQuery のようなSSRデータ取得をMSWでモックできるかは、E2EランナーがPlaywrightかCypressか で決まりません。実際には、Next.js dev serverプロセス内に msw/node を起動できるか で決まります。
サーバー側: msw/node(server.listen())→ SSR prefetch をインターセプト ✅
ブラウザ側: msw/browser(worker.start())→ クライアント fetch をインターセプト ✅
本記事の§5.1で示す構成(msw-provider.tsx で msw/node を起動)は、Cypress側でも成立します。start-server-and-test 等で同じNext.js dev serverを立ち上げる構成にすればよいだけです。ブラウザ側API(cy.intercept() と page.route())の違いはありますが、SSRモックの可否はランナー非依存です。
参考実装:
- billrisher/ssr-mocking(Cypress + Next.js SSR + msw/node)
- 9sako6/zenn.dev — Testing SSR Next.js with Cypress and MSW
コンポーネントテスト: Vitest を選んだ理由
プロジェクトの一部に既存のJestテストがありましたが、jest.mock() でフックを直接モックしており、E2Eで使うMSWとは別世界でした。
| |
Vitestにすると、E2Eと同じMSWハンドラーをそのまま使えます:
| |
| 観点 | jest.mock()(既存) | MSW(推奨) |
|---|---|---|
| モック対象 | TanStack Query のフック自体 | HTTP リクエスト |
| 検証範囲 | コンポーネントの描画のみ | fetch → フック → 描画の全フロー |
| E2E との整合性 | E2E は MSW、単体は jest.mock で別世界 | 同じハンドラーを共有 |
| リファクタリング耐性 | フック名変更で壊れる | API エンドポイントが変わらなければ安定 |
E2E と単体テストでモック基盤を共有できる のが最大の利点です。
エラー監視: Sentry を選んだ理由
テストでリリース前の品質は担保できますが、本番固有のエラーは必ず発生します。しかし、プロジェクトには 本番エラーを検知する仕組みが一切ない 状態でした。
Error BoundaryはフォールバックUIを表示するだけで、エラーの発生をどこにも通知しません。ユーザーからの問い合わせで初めてバグに気づく、という状況です。
Sentryを選んだ理由:
@sentry/nextjsがApp Routerをネイティブサポート(instrumentation.tsでSSR/RSCエラーも自動キャプチャ)- 既存の
react-error-boundaryにonErrorコールバックを追加するだけで統合可能 - Web Vitals(LCP / INP / CLS)の自動計測に対応(FIDは2024-03-12にINPへ置換され、Sentry SDK v10でも置換済み)
3. テストピラミッドの設計: 何をどのレイヤーでテストするか
「全ページをE2Eでテストする」のは非効率です。E2Eはテストピラミッドの頂点——最もコストが高いテスト手法です。
各層の特性は次のとおりです:
| 層 | 実行コスト | テスト本数の目安 | 主なテスト対象 |
|---|---|---|---|
| E2E | 数十秒〜分/テスト | 少(各サイト2〜3本) | ページ遷移を伴うクリティカルパス |
| コンポーネント・統合 | ミリ秒/テスト | 中量 | 状態遷移・エラーハンドリング |
| ユニット | ミリ秒/テスト | 多量 | Zodスキーマ・hooks・utils |
| 静的解析 | 即時(保存時 / CI) | 全コード | 型・構文・コーディング規約 |
ユースケース一覧から、以下の基準で振り分けました:
| E2E でやること | Vitest でやること |
|---|---|
| ページ遷移を伴うユーザーフロー | バリデーションの全パターン |
| 認証状態に依存する表示切り替え | ローディング・エラー・空状態 |
| API 連携の正常系ハッピーパス | API エラー系のハンドリング |
| クリティカルパス(複数ページ横断) | Zod スキーマの境界値 |
例えば「ログインフォームのバリデーション」はVitest、「トップ→一覧→詳細のページ遷移フロー」はE2E、という分担です。
全ページを最初からE2E化すると、CIの実行時間が伸び、テストが壊れるたびに修正コストが発生します。テストを書く文化を定着させる初期段階でこれをやると 「テスト=開発を止めるもの」 という認識が定着するリスクもあります。各サイト2〜3本のクリティカルパスからスタートし、段階的に拡充する計画です。
4. 3サイト横断のテスト基盤統一
3サイトとも Next.js App Router + TanStack Query + MSW という同じ技術スタックのため、テスト基盤を統一しました。同じ設計を別サイトでゼロから作り直すのは無駄です。
共通構成
| |
サイト別の設定差分
| 項目 | 利用者向けサイト | 提供者向けサイト | 管理画面 |
|---|---|---|---|
| ビューポート | モバイル専用(428×926) | PC + モバイル | PC + モバイル |
| 認証テスト | ユーザーログイン | 提供者ログイン | 管理者ログイン |
| 優先テスト対象 | 予約・決済フロー | シフト・稼働状態管理 | ユーザー管理・設定 |
設定差分はビューポートと認証フローの違い程度です。テスト基盤(Playwright設定、Vitest設定、MSW構成)はテンプレート化し、サイト間で統一しています。1サイトで構築 → 残り2サイトはテンプレート適用 のフローで横展開できます。
5. 実装: MSW × Playwright で SSR をテストする
ここからはPlaywrightを採用した前提に立ち、§2二次選考で確認した「ランナー非依存のSSR + MSW連携」を具体実装に落としていきます。
5.1 MSW の2層構造
Next.js App RouterでMSWを使うには、サーバー側とブラウザ側の両方 でMSWを起動する必要があります。
| |
| |
ブラウザ側だけでMSWを動かすと、SSR時の prefetchQuery が本物のAPIに飛んでしまいます。サーバー側にもMSWを入れることで、SSRでもモックデータが返ります。
onUnhandledRequest を "bypass" にする理由
dev serverとブラウザ側の両方で onUnhandledRequest: "bypass" を指定しているのは、Next.js内部の /_next/* を素通しする必要があるためです。素通しの対象はHMR・React Server Component fetch・Webpack/Turbopackチャンク取得などです。"error" にするとこれらの内部fetchがすべて未マッチで失敗扱いになり、dev server自体が壊れます。一方、後述する vitest.setup.ts 側はjsdomの純粋な環境です。未マッチfetch=バグとして "error" でfail-fastさせる方針にしています(§5.3)。
5.2 MSW ハンドラーを E2E と Vitest で共有する
テスト戦略全体で最も重要な設計判断は、E2E テストとコンポーネント・統合テストで MSW ハンドラーを共有する ことです。
| |
3サイトともOrval(OpenAPIコードジェネレーター)で自動生成されたMSWハンドラーを持っています。カスタムハンドラーを先頭に配置し、自動生成ハンドラーをフォールバックとして使う 構成です。
| |
この構成により、以下のメリットがあります:
- モックの二重管理が不要: E2E用とVitest用でハンドラーを別々に書く必要がない
- 正常系はそのまま動く: 既存のOrvalハンドラーがフォールバックとして機能するため、テストごとにモックを準備しなくてよい
- 異常系だけ上書きする:
server.use()でテスト単位でハンドラーを差し替えるだけで異常系テストが書ける
5.3 Vitest 側の MSW セットアップ
vitest.setup.ts でMSWサーバーのライフサイクルを管理します。
| |
vitest.config.ts でこのセットアップファイルを読み込みます:
| |
正常系テストでは server.use() を呼ぶ必要はありません。vitest.setup.ts で起動したMSWサーバーが、Orval自動生成ハンドラーのデフォルトレスポンスを返します。
SSRエラーハンドリングなど異常系テストでは、テスト単位で server.use() を使ってハンドラーを上書きします:
| |
5.4 Playwright 設定: webServer で MSW を起動する
Playwright側は webServer オプションでMSW有効のdev serverをテスト実行時に自動起動します。NEXT_PUBLIC_ENABLE_API_MOCKING=true でアプリ側のMSW ProviderをONにするのがポイントです。
| |
ビューポートはモバイル専用の利用者向けサイトの設定例です。PC・モバイル両対応のサイトでは projects にPCとモバイルの2つを並べる構成にしています(§4のサイト別差分テーブル参照)。
CI で workers: 1 にしている理由
§2の選定で「並列実行が無料」を決定打に挙げておきながら、上記configではCIに限り workers: 1 を指定しています。理由はMSWの状態競合の回避です。server.use() や server.resetHandlers() はプロセス内で共有されるグローバルstateを操作します。同一Next.js dev serverに対してテストを並列に走らせると、ハンドラーの上書きが相互に干渉します。今回はまず「19本のクリティカルパスをグリーンに通す」ことを優先し、安定性を取りました。
並列度を上げたい場合の選択肢は2つあります。
- dev serverを
workers数だけ起動して水平分割:playwright.config.tsのwebServerを複数起動するか、またはportを動的に割り当てる方法。各テストを独立したMSWプロセスへ振り分ける形になる - CI matrix で spec単位の分割: GitHub Actions / CircleCIのmatrix機能でspecファイルをチャンク化し、ジョブを並列実行する
今回は19本・40秒のスケールでは投資効果が薄かったため workers: 1 を選びました。本数が増えてきたタイミングで上記いずれかに切り替えます。
package.json 側のスクリプトはこうなります:
| |
CIでは pnpm test と pnpm e2e を順に実行するだけです。
5.5 networkidle は使わない
Playwrightの waitForLoadState('networkidle') は、ネットワークリクエストが一定時間途絶えるまで待機する機能です。一見便利ですが、不安定なテストの原因 です。
| |
5.6 既知の制約
テスト基盤の設計段階で明らかになった制約と、その対策を整理しておきます:
| 制約 | 影響 | 対策 |
|---|---|---|
SSR prefetch に page.route() が効かない | E2E でエラー系テストが偽陽性になる | Vitest + MSW で SSR エラーテストを補完 |
| MSW モックのためサイト間データ連携の検証不可 | サイトをまたいだデータ整合性は E2E では検証できない | シナリオテスト(手動)およびユースケーステスト(バックエンド)で担保 |
waitForLoadState('networkidle') が不安定 | MSW 環境でテストがフレーキーになる | waitForSelector や waitForResponse で特定の要素・API を待つ |
特に サイト間データ連携 はE2E(MSWモック)の根本的な限界です。利用者向けサイトでの操作が提供者向けサイトに反映されるかといった検証は、バックエンドのユースケーステストや手動のシナリオテストで担保する設計です。
6. Sentry を本番運用に乗せる設計
Sentryは アプリ内のあらゆるデータを SaaS に送る 仕組みです。送信対象は、エラーのスタックトレース・HTTPリクエストの内容・ユーザー入力(breadcrumb)・Session ReplayのDOMなど多岐にわたります。デフォルト設定のまま本番に出すと、認証トークン・個人情報・業務上の機密データがSentryのサーバー側に保存され、漏洩時のリスクが大きくなります。
特にチャット・通話・問い合わせフォームなどユーザー間のコミュニケーションを扱うサービスでは、本文・電話番号・決済情報がbreadcrumbやフォーム値経由でSentryに届きやすい構造です。
このセクションでは、Turbopack環境で初期化に詰まったポイントを共有します。さらに、本番運用に乗せるための 3層防御のマスキング、ソースマップを Sentry にアップロードしない選択肢、そして リリース前チェックリスト も解説します。
6.1 Turbopack で instrumentation-client.ts を使う
これが最もつまずいたポイントです。
Sentryの公式ドキュメントに従って sentry.client.config.ts を作成しましたが、ブラウザの Console に Sentry のログが一切出ません でした。
原因は Next.js 16 + Turbopack 環境では、sentry.client.config.ts が自動読み込みされないことでした。
Sentryの @sentry/nextjs はwebpackのエントリポイントに sentry.client.config.ts を自動注入する仕組みです。しかし、Turbopackはこのwebpackプラグインを使いません。
解決策: ファイルを instrumentation-client.ts にリネームする。
❌ sentry.client.config.ts → Turbopack では読み込まれない
✅ instrumentation-client.ts → Turbopack の client instrumentation として認識される
instrumentation-client.ts はNext.jsの公式APIであり、Turbopackでもクライアント側の初期化コードとして確実に実行されます。
リネーム後にdev serverを再起動したところ、ConsoleにSentryのトレースログが大量に出力され、正常動作を確認できました。
| |
Sentry SDKを確認すると、webpackビルドでは2ファイルを順番に検索する実装でした。sentry.client.config.ts → instrumentation-client.ts の順です。後者が存在すれば前者の非推奨警告を出す挙動です。Turbopack移行を見据えると、今後は instrumentation-client.ts を使うのが正解です。
サーバー側は instrumentation.ts でSSR/RSC由来のエラーを捕捉します。onRequestError フックを定義しておくと、SSRレンダリング中の例外もSentryに届きます:
| |
instrumentation-client.ts(ブラウザ側)と instrumentation.ts(サーバー側)の2ファイルが揃って、はじめてApp Routerのエラー全域がカバーされます。
6.2 マスキング設計: 3層防御で機密データを Sentry に送らない
Sentryの beforeSend フックで簡易マスクをかけている記事は多く見かけます。しかし、公式の “Scrubbing Sensitive Data” を基準に再設計したところ、beforeSend だけではギャップが残る と判明しました。
漏れがちな箇所:
event.user(Sentry.setUserの値)- スタックトレースに含まれるローカル変数値(
frame.vars) event.extra/event.contexts/event.tagsconsoleカテゴリのbreadcrumb- Session ReplayのDOMテキスト・入力値
これらをすべてカバーするため、SDK側・Sentryサーバ側・組織設定の3層で防御する設計にしました。
%%{init: {"flowchart": {"htmlLabels": true, "nodeSpacing": 60, "rankSpacing": 80, "padding": 20}, "themeVariables": {"fontSize": "15px"}}}%%
flowchart LR
A["アプリ<br/>(エラー発生)"]
L1["第1層: SDK フック<br/>beforeSend / beforeBreadcrumb<br/>送信前にマスク"]
B["Sentry サーバ<br/>(受信)"]
L2["第2層: Server-side<br/>Data Scrubber<br/>保管前に追加キーをマスク"]
C["永続化 / UI 表示"]
L3["第3層: Generative AI<br/>機能停止<br/>組織設定 OFF + 学習オプトアウト"]
A --> L1 --> B --> L2 --> C
L3 -. 組織設定 .-> B
classDef stage fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#0d47a1,padding:12px
classDef layer fill:#fff3e0,stroke:#e65100,stroke-width:2.5px,color:#bf360c,padding:14px
classDef result fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px,color:#4a148c,padding:12px
class A,B stage
class L1,L2,L3 layer
class C result
| 層 | 防御内容 | 設定箇所 | 役割 |
|---|---|---|---|
| 第1層 | SDK の beforeSend / beforeSendTransaction / beforeBreadcrumb で送信前にマスク | アプリコード | 送信そのものを止める最強の防御 |
| 第2層 | Sentry 組織の Data Scrubber を有効化、追加キーを登録 | Sentry UI | 第1層の漏れを最終ガード。SDK 改修なしで即時更新可 |
| 第3層 | Generative AI 機能を組織全体で停止、学習利用はオプトアウト維持 | Sentry UI | データの AI 学習利用を明示的に防ぐ |
第1層: SDK フックの実装方針
Sentry.init のオプションと beforeSend / beforeBreadcrumb で以下を徹底します:
| |
設計のポイントは カスタム拡張に限ったホワイトリスト方式 です。SDKが自動付与する runtime / os / browser / trace 等は調査・tracingに不可欠なので保持します。そのうえで、アプリ側で Sentry.setContext などで追加したカスタム値だけ許可キーで絞ります。「機密キーをブラックリストで弾く」とリスト漏れで漏洩しますが、「許可キーだけ通す」設計なら新しいキーが追加されたときに自動的にブロックされます。許可キーの追加はADRで履歴を残し、レビューを経てから反映する運用にしています。
pickAllowed ヘルパーはコピペで動くようにジェネリックで定義しておきます。
| |
参考: Sentry Dev Docs — Browser Tracing
第1層補足: Session Replay は必ずマスクを有効化する
Session Replayを使うなら、DOM上のテキストや入力値が録画されないように以下を必須にします:
| |
「特定の要素だけ表示する」運用にしたい場合も、デフォルトでマスクし、必要な要素だけ解除する 方向にすると安全です。逆方向(デフォルト表示・機密要素だけブロック)はクラス付け漏れで簡単に破綻します。
第2層: Server-side Data Scrubber
第1層がSDKのバグや実装漏れで効かなかったときの最終ガードとして、Sentry組織側でもServer-side Scrubberを有効化します。
Sentry UIの Organization Settings → Security & Privacy で以下を設定:
- Data Scrubber: ON
- Use Default Scrubbers: ON: 既定キー(
password/token/api_key/access_token/auth等)をマスク - Additional Sensitive Fields にサービス固有の機密フィールド名を追加。例:
- 連絡先系:
telephone/phone/tel/email/birthday - ID系:
user_id/account_id - コンテンツ系:
message/comment/memo - 決済系:
card_number/payment_method/order_amount
- 連絡先系:
- Prevent Storing of IP Addresses: ON
第1層と第2層を両方使う最大の利点は、SDK の再リリース不要で即座にマスク追加できる 点です。新しい機密フィールドが見つかったとき、Sentry UIで1行追加すれば全サイトに即時反映されます。
第3層: Generative AI 機能の停止
SentryにはSeer(Auto-Fix, Issue Summary等)というGenerative AI機能があります。エラー内容をAIが要約・解析する便利な機能ですが、機密データを含むエラーが AI に渡される可能性 があります。
機密度の高いサービスでは、組織全体で停止するのが安全です:
- Organization Settings → “Show Generative AI Features” を OFF
- Seerの各機能(Auto-Fix, Issue Summary等)も個別にOFF
- データのAI学習利用はデフォルトで opt-out(SentryのAI Privacy Principles参照)だが、設定画面でオフ状態を確認しスクリーンショットを残す
Sentryのプライバシーポリシーには次の記載があります。
By default, your data will not be used to train any generative AI models without your permission
とはいえ、念のため設定画面で オフ状態を確認・スクリーンショットを保管 しておくと、社内のセキュリティ監査や問い合わせがあったときに即答できます。
6.3 ソースマップを Sentry にアップロードしないという選択肢
Sentryのドキュメントに従えば、CIでソースマップをアップロードしてブラウザに公開しない(hidden-source-map)構成が標準です。Sentry上でスタックトレースが元のソースコードに解決され、調査が一気に速くなります。
しかし、ソースマップそのものを Sentry に置かない という選択肢もあります。
ソースマップに含まれる関数名・変数名・コード断片そのものが「Sentryに置きたくない情報」になりえる場合です。万が一Sentry側で漏洩が起きたとき、ソースマップがあるとアプリの内部実装が完全に復元できてしまいます。
ローカルで手動解決する運用
そこで、機密度の高いサービスでは以下の運用に切り替える選択肢があります:
- ビルド時に
.next/配下へソースマップを生成する(ローカル保持) withSentryConfigのsourcemaps.disableをtrueに固定し、Sentryへのアップロードを止める- エラー解析時はローカルでソースマップを使って手動解決
具体的な設定は次の通りです。
| |
参考: Sentry Build Options — sourcemaps.disable / Source Maps (Next.js)
flowchart TD
A["Sentry でエラー検知"] -->|Slack 通知| B["担当が気づく"]
B --> C{".next/ に .map があるか?"}
C -->|ある| E["③ source-map ライブラリで<br/>元ファイル・行番号を解決"]
C -->|なければ| D["② git checkout <該当 release のコミット><br/>&& pnpm build"]
D --> E
E --> F["④ 該当コードのスニペットを表示"]
このフローはスクリプト化(あるいはSlackスラッシュコマンド化)して、誰でもワンコマンドで回せる状態にしておくのが必須です。手動解決のままだと属人化します。
トレードオフと対策
| 課題 | 対策 |
|---|---|
| エラー調査時に手動解決の手間が入る | スクリプト化(/resolve-sourcemap 的なコマンド)でワンコマンド化 |
| 担当者の手元ビルド環境が必要 | git checkout main && pnpm build を標準フローに含め、環境変数を固定 |
| 過去リリースの再現性 | エラーの release タグからコミットハッシュを引いてビルド |
| 属人化 | スクリプトとデモ動画をチームに共有して、複数人が回せる状態を担保 |
調査効率は当然落ちます。しかし、「便利さ」と「漏洩時の影響範囲を最小に抑えること」のトレードオフ で後者を取る判断です。Sentryの標準構成が常に正解とは限りません。
6.4 リリース前チェックリスト
Sentryを本番運用に乗せる前は、以下を毎回確認します:
-
beforeSendの単体テストがPASS(マスキング処理が想定通り動くか) -
sendDefaultPii: falseがSentry.initに明示されている - ステージングで実エラーを発生させ、Sentry上のeventに PII / 認証情報が含まれないこと をサンプル確認(最低5件)
- Sentry UIのData ScrubberがON、追加キーが登録済み
- Generative AI機能がOFF、学習利用がオプトアウト状態
- ソースマップがSentryにアップロードされていない(環境変数・ビルド設定で固定)
- エビデンス(スクリーンショット・サンプルイベント)を社内ドキュメントに格納
特にサンプル確認は重要です。beforeSend を書いただけでは、本当に消えているかはSentry上で見てみないと分かりません。「実装した」と「機能している」は別物として運用に組み込みます。
7. コスト: テストを書く工数に見合うか
「テストを書く時間でバグ修正した方が早くない?」という疑問への回答です。
定性面の比較
| テストなし(導入前) | テストあり(導入後) | |
|---|---|---|
| リグレッション検知 | 手動で全画面再確認(30分〜1時間/回) | CI で自動検知(数分) |
| バグ修正後の再確認 | 修正者が手動で再テスト | pnpm e2e で即確認 |
| 本番エラー検知 | ユーザー報告経由(数時間〜数日遅れ) | Sentry でリアルタイム通知 |
導入工数(実測)
| 作業 | 工数 | 成果物 |
|---|---|---|
| Vitest 導入 + スキーマテスト13本 | 1〜2時間(AI 生成 + レビュー) | Zod スキーマ13本、PASS 13/13 |
| E2E テスト6本(検索→詳細フロー) | 1〜2時間(AI 生成 + デバッグ) | 6本中6本グリーン、実行時間約40秒 |
| Sentry 導入 + ErrorBoundary 統合 | 1〜2時間(wizard + Turbopack 対応) | 3サイト分の instrumentation-client.ts 整備 |
| 合計 | 約半日(4〜6時間) | テスト19本 + 監視基盤 |
数字で見る回収ライン
導入前は1回の手動再確認に30分〜1時間かかっていました。導入後はCIによる自動検知に置き換わり、再確認時間はゼロです。バグ修正→デグレ確認のループが1日あたり3〜5回発生していた状況を踏まえると、1〜2日で導入工数を回収できる試算 です。
実装の大半はAI(Claude Code)で生成し、人間はレビューと動作確認が中心です。configの生成、テストコードの雛形作成、Zodスキーマからのテストケース導出はAIが得意な作業です。
8. 属人化防止: 自分だけがテストを書ける状態にしない
テスト基盤を導入しても、特定の人しかテストを書けない状態では意味がありません。
テンプレートの標準化
Playwrightのconfig、MSWの連携パターン、Vitestのセットアップファイルはすべてテンプレート化し、新しいサイトへの横展開はテンプレートのコピーで完了するようにしました。
AI による量産フロー
インプット:
1. 対象コンポーネントのソースコード
2. MSW ハンドラー(モックデータ)
3. テスト項目(ユースケースから振り分けたもの)
→ AI が .test.tsx / .spec.ts を生成
→ 人間がレビュー + 動作確認
CI でのゲート
テストをCIに組み込むことで、テストが壊れたら PR を出した人が修正する 運用になります。テスト作成者だけが保守する状態にはなりません。
9. AI 生成テストの落とし穴: 「成功するテストしか作らない」問題
AIでテストを量産する際に気づいた重要な課題があります。
問題: コードから生成すると「バグを正解として固定する」
AIにソースコードを渡して「このコンポーネントのテストを書いて」と指示すると、現在の実装を正解とするテスト が生成されます。
ソースコード → AI → テスト生成
→ 「この実装の振る舞いが正しい」前提で期待値を設定
→ バグがあってもテストは通る
これは「テスト成功=品質の担保」という誤った安心感を生みます。
実体験: 同じ AI でも、入力が違えば結果が変わる
今回の導入では、Zodスキーマテスト13本とE2Eページ遷移フロー6本をAIで生成しました。結果は対照的でした:
- Zod スキーマテスト: スキーマ定義そのものが「仕様」のため、コードから生成して問題なし。13本中13本が意図通りのテストになった
- E2E ページ遷移フロー:「ユーザーがどう動線を通るか」という仕様情報を渡さなかったため、AIは実装の振る舞いをそのままテスト化。実装 PoC の域を出ない品質 にとどまった
→ 仕様情報を持たないテストは、実装が変わったときに「正しく壊れる」ことができません。これがPhase 2以降は仕様書ベースで生成すべき理由です。
対策: テストのインプットを「コード」ではなく「仕様」にする
❌ コード → AI → テスト(実装が正解になる)
✅ 仕様書 → AI → テスト → コードに対して実行(仕様が正解になる)
仕様書ベースでテストを先に書けば、テストが落ちたときに「コード側のバグ」と判断できます。本来のテストの使い方です。
使い分けの基準
すべてのテストを仕様ベースで書く必要はありません。テストの種類によって使い分けます。
| テストの種類 | コードから生成してOK | 仕様から生成すべき |
|---|---|---|
| Zod スキーマバリデーション | ✅ スキーマ定義=仕様そのもの | — |
| スナップショットテスト | ✅ リグレッション検知が目的 | — |
| E2E ページ遷移フロー | — | ✅ ユーザーの期待する動線 |
| 異常系・境界値テスト | — | ✅ 仕様上の制約を検証 |
段階的な進め方
Phase 1(基盤構築期): コードから生成 → リグレッション防止用
- 「今の動作を壊さない」ためのテスト
- 基盤の証明が目的なのでこれで OK
Phase 2(テスト拡充期): 仕様書から生成 → バグ発見用
- ユースケース一覧をインプットに
- AI にコードは渡さず「この仕様を満たすテストを書いて」と指示
- テストが落ちたらコード側のバグ
Phase 3(運用期): 両方を組み合わせ
- 仕様ベーステスト: 新機能・重要フロー
- コードベーステスト: リファクタリング時のリグレッション防止
今回の導入では、基盤の証明が目的だったのでPhase 1(コードベース)で問題ありませんでした。次フェーズでは仕様書(ユースケース一覧)をインプットとする運用へ移行します。
まとめ
テスト基盤をゼロから整備して学んだことをまとめます。
- Playwright 選定の決め手は AI 連携・並列実行・マルチブラウザ — SSR + MSWのサポートは「
msw/nodeをdev serverに同居させる設計」がコア。ランナー(Playwright/Cypress)には依存しない。3サイト横断でスケールするAI連携と並列実行が決定打 - 全ページ E2E にしない — テストピラミッドの原則に従い、クリティカルパス(ページ遷移フロー)はE2E、バリデーション等はVitestで分担する
- MSW ハンドラーを E2E と Vitest で共有する —
[...customHandlers, ...orvalHandlers]でカスタム優先・自動生成フォールバックの構成にする。モックの二重管理がなくなる - MSW の2層構造は必須 — サーバー側(
msw/node)とブラウザ側(msw/browser)の両方でMSWを起動しないと、SSR prefetchにモックが効かない - Turbopack 時代の正解は
instrumentation-client.ts— webpack専用のsentry.client.config.tsはTurbopackで読み込まれない - Sentry のマスキングは
beforeSendだけでは不十分 — SDKフック・組織側Server-side Scrubber・Generative AI機能の停止の3層で設計する。extra/contexts/tagsはホワイトリスト方式が安全 - ソースマップを Sentry にアップロードしない選択肢もある — 機密度の高いサービスでは、ローカル保持+手動解決スクリプトで運用する方が漏洩時の影響範囲を抑えられる
- AI でテストを生成するなら「コード」ではなく「仕様」を起点に — コードから生成するとバグを正解として固定する。仕様書ベースで生成すれば、テスト失敗=コードのバグとして機能する
- AI で量産、人間はレビュー — config生成もテストコード作成もAIが得意。半日で3ツールの導入が完了した
テスト基盤は「1本目のテストを書くまで」が最も重いです。仕組みさえできれば、あとはテンプレートとAIで量産できます。
次回予告: AI(Claude Code / Devin / NotebookLM)を活用したテスト仕様書作成の実践。「コードから生成」を「仕様から生成」に切り替える具体的なワークフローを紹介予定です。
