GA4の「行動フロー」レポートは廃止され、現在のGA4 Data APIではユーザーレベルの遷移シーケンスを取得する手段がありません。 「/blog/ を見たユーザーは次にどこへ行くのか」「問い合わせに至るまでの経路は何か」こういった分析をClaude Codeの自然言語で分析できるようにするには、BigQuery Exportの生イベントデータが必要です。
本記事では、TypeScriptで実装したGA4 MCPサーバーに analyze_behavior_flow ツールを追加し、Claude Codeから自然言語で行動フロー分析を呼び出せる仕組みを構築した方法を共有します。
対象読者
- GA4 BigQuery Exportのデータを活用したい方
- MCPサーバーに外部データソースを追加する設計パターンに興味がある方
- BigQueryをNode.js(TypeScript)から扱う実践例を探している方
1. なぜGA4 Data APIだけでは不十分か
GA4 Data APIが返すのは集計済みデータです。
1sessionSource / sessionMedium / sessions / bounceRate ...セッション単位での指標は取得できますが、「あるページを閲覧したユーザーが、その後どのページに遷移したか」というシーケンスは取得できません。これはGA4のデータモデル上の制約です。
一方、BigQuery Exportには生イベントデータが蓄積されます。
1-- events_YYYYMMDD テーブルのイメージ2event_date | event_name | user_pseudo_id | event_params(ARRAY)32026-01-15 | page_view | abc123... | [{key: "page_location", value: "https://..."}, ...]42026-01-15 | page_view | abc123... | [{key: "page_location", value: "https://.../leasing/"}, ...]同一ユーザーの user_pseudo_id + ga_session_id で結合すれば、セッション内のページ遷移順序を正確に再現できます。
2. システム全体のアーキテクチャ
今回追加したコンポーネントの配置は以下のとおりです。
1Claude Code (自然言語)2 │3 ▼4┌─────────────────────────────────────────┐5│ GA4 Analyzer MCPサーバー │6│ ┌──────────────────────────────────┐ │7│ │ analyze_behavior_flow (NEW) │ │8│ │ MCPツール層 / Zodバリデーション │ │9│ └──────────────┬───────────────────┘ │10│ │ │11│ ┌──────────────▼───────────────────┐ │12│ │ BehaviorFlowAnalyzer (NEW) │ │13│ │ application層 / 5軸分析ロジック │ │14│ └──────────────┬───────────────────┘ │15│ │ │8 collapsed lines
16│ ┌──────────────▼───────────────────┐ │17│ │ BigQueryClient (NEW) │ │18│ │ infrastructure層 │ │19│ └──────────────┬───────────────────┘ │20└─────────────────┼───────────────────────┘21 ▼22 Google BigQuery API23 analytics_318772207.events_*既存の4ツール(analyze_comparison など)には一切変更を加えず、新規ファイルの追加と既存ファイルへの追記のみで実装しました。
3. BigQueryClient の実装
認証設計
既存の GA4_SERVICE_ACCOUNT_KEY をそのまま流用します。新しいキーファイルは不要です。
1import { BigQuery } from "@google-cloud/bigquery";2import type { BQConfig } from "@/types/models";3
4export class BigQueryClient {5 private readonly client: BigQuery;6 private readonly config: BQConfig;7
8 constructor(config: BQConfig, keyFilePath: string) {9 this.config = config;10 this.client = new BigQuery({11 projectId: config.projectId,12 keyFilename: keyFilePath || undefined, // 空文字の場合はADCにフォールバック13 location: config.location,2 collapsed lines
14 });15 }コスト制御
BigQueryはオンデマンド課金です。長期間のクエリをかけると予期しないコストが発生します。maximumBytesBilled を全クエリに強制適用することで、コスト暴走を防いでいます。
1async query<T>(2 sql: string,3 params: Record<string, string | number | boolean | string[]>,4): Promise<T[]> {5 const [rows] = await this.client.query({6 query: sql,7 params,8 maximumBytesBilled: String(this.config.maxBytesBilled), // デフォルト10GB9 useLegacySql: false,10 location: this.config.location,11 });12 return rows as T[];13}上限を超えるクエリは実行前にBigQueryがエラーを返します。これをキャッチして BigQueryCostLimitError に変換しています。
1if (message.includes("bytesBilledExceeded")) {2 throw new BigQueryCostLimitError(this.config.maxBytesBilled);3 // → "BigQueryのスキャンバイト上限(10GB)を超過します。期間を短縮するか..."4}SQLインジェクション対策
@google-cloud/bigquery のパラメータ化クエリを使い、ユーザー入力を直接SQLに埋め込まない設計にしています。
1// BAD: 文字列結合(インジェクション可能)2const sql = `WHERE page_location LIKE '%${targetPath}%'`;3
4// GOOD: パラメータ化クエリ5const sql = `WHERE page_location LIKE CONCAT('%', @target_path, '%')`;6await client.query(sql, { target_path: targetPath });4. GA4 BigQuery Exportのスキーマを扱う上での注意点
GA4のBigQueryスキーマは独特です。event_params が ARRAY<STRUCT<key STRING, value STRUCT<...>>> という入れ子構造になっています。
1-- page_location を取得するには UNNEST が必要2SELECT3 (SELECT ep.value.string_value4 FROM UNNEST(event_params) AS ep5 WHERE ep.key = 'page_location') AS page_location6FROM `project.dataset.events_*`7WHERE _TABLE_SUFFIX BETWEEN @start_suffix AND @end_suffixまた、ga_session_id もこの event_params の中に入っています。セッションを結合する際は必ず user_pseudo_id + ga_session_id の複合キーを使う必要があります。
1-- ユーザーとセッションを正確に特定する2CONCAT(user_pseudo_id, '-', CAST(3 (SELECT ep.value.int_value FROM UNNEST(event_params) AS ep4 WHERE ep.key = 'ga_session_id')5 AS STRING6)) AS session_keyコスト管理のために _TABLE_SUFFIX BETWEEN @start_suffix AND @end_suffix を全クエリに必ず含めるのも重要です。これがないとテーブル全件スキャンになります。
5. BehaviorFlowAnalyzer — 5軸分析の設計
並列実行
5軸の分析クエリは互いに独立しているため、Promise.all で並列実行しています。
1const [meta, entryPaths, exitPaths, sessionPaths, segmentComparison] =2 await Promise.all([3 this.queryMeta(startSuffix, endSuffix, input),4 this.queryEntryPaths(startSuffix, endSuffix, input),5 this.queryExitPaths(startSuffix, endSuffix, input),6 this.querySessionPaths(startSuffix, endSuffix, input),7 this.querySegmentComparison(startSuffix, endSuffix, input),8 ]);これにより、5クエリが並列で実行され、応答時間を短縮できます。コンバージョン経路クエリだけは includeConversion フラグで制御し、不要な場合はスキップします。
エントリーパス分析のSQLパターン
「直前ページ」を取得するには、セッション内でタイムスタンプが対象ページより前のpage_viewを ROW_NUMBER() で順位付けし、rn = 1(最も近いもの)を取り出します。
1WITH target_hits AS (2 SELECT user_pseudo_id, session_id, event_timestamp3 FROM `project.dataset.events_*`4 WHERE _TABLE_SUFFIX BETWEEN @start_suffix AND @end_suffix5 AND event_name = 'page_view'6 AND page_location LIKE CONCAT('%', @target_path, '%')7),8prev_pages AS (9 SELECT10 REGEXP_REPLACE(page_location, r'^https?://[^/]+', '') AS path,11 ROW_NUMBER() OVER (12 PARTITION BY user_pseudo_id, session_id13 ORDER BY event_timestamp DESC -- 対象ページに最も近い順14 ) AS rn15 FROM `project.dataset.events_*` AS e12 collapsed lines
16 JOIN target_hits AS t17 ON e.user_pseudo_id = t.user_pseudo_id18 AND e.session_id = t.session_id19 AND e.event_timestamp < t.event_timestamp -- 対象ページより前20 WHERE event_name = 'page_view'21)22SELECT path, COUNT(*) AS sessions23FROM prev_pages24WHERE rn = 1 -- 直前ページのみ25GROUP BY path26ORDER BY sessions DESC27LIMIT @top_nセッション完全パスのSQLパターン
ARRAY_AGG(... ORDER BY event_timestamp) でセッション内のページ遷移を配列にまとめます。
1SELECT2 ARRAY_AGG(3 REGEXP_REPLACE(page_location, r'^https?://[^/]+', '')4 ORDER BY event_timestamp5 ) AS page_sequence,6 COUNT(*) AS sessions7FROM session_pages8GROUP BY ARRAY_TO_STRING(page_sequence, ' > ')9ORDER BY sessions DESC10LIMIT @top_nこれにより ["/top/", "/leasing/", "/contact/"] のような遷移パターンを集計できます。
6. MCPツール層 — 既存アーキテクチャへの追加方法
MCPツールの追加は3ステップです。既存パターンに合わせた実装なので、迷う箇所はありませんでした。
Step 1: Zodスキーマ定義(src/mcp/utils/validator.ts に追記)
1export const AnalyzeBehaviorFlowInputSchema = z.object({2 targetPath: z.string()3 .min(1)4 .refine(v => v.startsWith("/"), "スラッシュ始まりにしてください"),5 dateRange: z.object({6 startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),7 endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),8 }),9 options: z.object({10 userSegment: z.enum(["all", "new", "returning"]).default("all"),11 topN: z.number().int().min(1).max(50).default(10),12 // ...13 }).default({ ... }),14});Step 2: ハンドラー実装(src/mcp/tools/analyze-behavior-flow.ts を新規作成)
1async function handleAnalyzeBehaviorFlow(2 input: AnalyzeBehaviorFlowInput,3): Promise<MCPResponse<BehaviorFlowResult>> {4 try {5 const envConfig = loadConfigFromEnv();6
7 if (!envConfig.bigQuery) {8 throw new BigQueryNotConfiguredError([9 "GA4_BQ_PROJECT_ID", "GA4_BQ_DATASET_ID",10 ]);11 }12
13 const bqClient = new BigQueryClient(14 envConfig.bigQuery,15 envConfig.serviceAccountKeyPath ?? "",11 collapsed lines
16 );17 await bqClient.validateDataset();18
19 const analyzer = new BehaviorFlowAnalyzer(bqClient, bqClient.tableWildcard);20 const result = await analyzer.analyze({ ...input });21
22 return { success: true, data: result };23 } catch (error) {24 return toMCPError(error as Error);25 }26}Step 3: ツール登録(src/mcp/tools/index.ts に1行追加)
1return [2 createAnalyzeExploratoryTool(AnalyzeExploratoryInputSchema),3 createAnalyzeComparisonTool(AnalyzeComparisonInputSchema),4 createCheckConfigTool(CheckConfigInputSchema),5 createValidateConfigTool(ValidateConfigInputSchema),6 createAnalyzeBehaviorFlowTool(AnalyzeBehaviorFlowInputSchema), // ← 追加7];7. エラー処理の設計 — 継承を活かしたマッピング
BigQuery固有のエラークラスは既存のエラー階層に組み込みました。
1GA4AnalyzerError2├── ConfigurationError3│ └── BigQueryNotConfiguredError ← BQ環境変数未設定4├── ApiError5│ ├── QuotaExceededError6│ │ └── BigQueryCostLimitError ← スキャンバイト超過7│ └── BigQueryError ← BQ一般エラーtoMCPError() では具体的なクラスを先にチェックすることで、汎用クラスへの誤マッチを防いでいます。
1// NG: ConfigurationError を先にチェックすると BigQueryNotConfiguredError が2// ここに引っかかり、BigQuery専用のメッセージが返らない3if (error instanceof ConfigurationError) { ... } // 汎用4if (error instanceof BigQueryNotConfiguredError) { ... } // 具体的5
6// OK: 具体的なものから先にチェック7if (error instanceof BigQueryNotConfiguredError) { ... } // 具体的8if (error instanceof BigQueryCostLimitError) { ... } // 具体的9if (error instanceof BigQueryError) { ... } // 中間10if (error instanceof ConfigurationError) { ... } // 汎用(フォールバック)8. テスト戦略 — BigQueryのモック
@google-cloud/bigquery の BigQuery クラスをモックする際、vitest 4.x では function キーワードが必要です。
1// vitest 4.x での class mock2const mockDatasetExists = vi.fn().mockResolvedValue([true]);3const mockBigQueryQuery = vi.fn().mockResolvedValue([[{ result: 1 }]]);4
5vi.mock("@google-cloud/bigquery", () => {6 const BigQuery = vi.fn(function (this: Record<string, unknown>) {7 // function キーワードが必要(arrow function ではコンストラクタとして使えない)8 this.dataset = vi.fn().mockReturnValue({ exists: mockDatasetExists });9 this.query = mockBigQueryQuery;10 });11 return { BigQuery };12});モック変数をトップレベルで宣言しておくと、各テストから mockDatasetExists.mockResolvedValueOnce([false]) のように個別のテストケースを設定できます。
9. 設定と使い方
mcp_config.json への追加
1{2 "mcpServers": {3 "ga4-analyzer": {4 "command": "node",5 "args": ["/path/to/dist/mcp-server.js"],6 "env": {7 "GA4_PROPERTY_ID": "318772207",8 "GA4_SERVICE_ACCOUNT_KEY": "/path/to/service-account.json",9 "GA4_BQ_PROJECT_ID": "your-gcp-project",10 "GA4_BQ_DATASET_ID": "analytics_318772207",11 "GA4_BQ_LOCATION": "asia-northeast1",12 "GA4_BQ_MAX_BYTES_BILLED": "10737418240"13 }14 }15 }1 collapsed line
16}Claude Codeからの呼び出し例
1/leasing/ ページを閲覧したユーザーの行動フローを過去30日で分析して2
3analyze_behavior_flow を実行:4 targetPath: "/leasing/"5 dateRange: { startDate: "2025-12-10", endDate: "2026-01-09" }6 options: { userSegment: "all", topN: 10, includeConversion: true }返ってくるデータ(抜粋):
1{2 "meta": { "totalSessions": 342, "totalUsers": 298 },3 "entryPaths": [4 { "rank": 1, "path": "/", "sessions": 142, "percentage": 41.5 },5 { "rank": 2, "path": "/news/", "sessions": 87, "percentage": 25.4 }6 ],7 "exitPaths": [8 { "rank": 1, "path": "(exit)", "sessions": 198, "percentage": 57.9 },9 { "rank": 2, "path": "/contact/", "sessions": 54, "percentage": 15.8 }10 ],11 "segmentComparison": {12 "new": { "bounceRate": 74.2, "avgSessionDurationSec": 98 },13 "returning": { "bounceRate": 31.5, "avgSessionDurationSec": 215 }14 },15 "insights": [4 collapsed lines
16 "新規ユーザーの直帰率が74.2%と高い状態です。ページコンテンツや導線の見直しを検討してください。",17 "リピーターの直帰率(31.5%)は新規(74.2%)より大幅に低く、サイト習熟度の差が表れています。"18 ]19}まとめ
今回の実装を通じて得た知見をまとめます。
学んだこと
-
GA4のBigQueryスキーマは UNNEST が要る —
event_paramsの入れ子構造をあらかじめ把握してから実装を始めることが重要。INFORMATION_SCHEMA.COLUMNSでスキーマを確認してから設計するのが確実。 -
maximumBytesBilledは必須 — コスト管理なしでBigQueryを使うのは危険。デフォルト10GBを設定し、超過時は詳細なエラーメッセージを返す。 -
vitest 4.x での class mock は
functionキーワード — arrow functionでコンストラクタをモックするとwarningが出て挙動が不安定になる。 -
エラー継承チェックは具体的なものから —
instanceofチェックの順序で親クラスのマッチングを防ぐ。 -
MCPツール追加は3ステップ — Zodスキーマ → ハンドラー実装 → 登録。既存パターンに倣えばスムーズ。