45395 - シコウサクゴ -

GA4 MCPサーバーにBigQuery Exportを統合して「ユーザー行動フロー分析」を追加して自然言語で分析できるようにした

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が返すのは集計済みデータです。

1
sessionSource / sessionMedium / sessions / bounceRate ...

セッション単位での指標は取得できますが、「あるページを閲覧したユーザーが、その後どのページに遷移したか」というシーケンスは取得できません。これはGA4のデータモデル上の制約です。

一方、BigQuery Exportには生イベントデータが蓄積されます。

1
-- events_YYYYMMDD テーブルのイメージ
2
event_date | event_name | user_pseudo_id | event_params(ARRAY)
3
2026-01-15 | page_view | abc123... | [{key: "page_location", value: "https://..."}, ...]
4
2026-01-15 | page_view | abc123... | [{key: "page_location", value: "https://.../leasing/"}, ...]

同一ユーザーの user_pseudo_id + ga_session_id で結合すれば、セッション内のページ遷移順序を正確に再現できます。


2. システム全体のアーキテクチャ

今回追加したコンポーネントの配置は以下のとおりです。

1
Claude 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 API
23
analytics_318772207.events_*

既存の4ツール(analyze_comparison など)には一切変更を加えず、新規ファイルの追加と既存ファイルへの追記のみで実装しました。


3. BigQueryClient の実装

認証設計

既存の GA4_SERVICE_ACCOUNT_KEY をそのまま流用します。新しいキーファイルは不要です。

src/infrastructure/bigquery-client.ts
1
import { BigQuery } from "@google-cloud/bigquery";
2
import type { BQConfig } from "@/types/models";
3
4
export 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全クエリに強制適用することで、コスト暴走を防いでいます。

1
async 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), // デフォルト10GB
9
useLegacySql: false,
10
location: this.config.location,
11
});
12
return rows as T[];
13
}

上限を超えるクエリは実行前にBigQueryがエラーを返します。これをキャッチして BigQueryCostLimitError に変換しています。

1
if (message.includes("bytesBilledExceeded")) {
2
throw new BigQueryCostLimitError(this.config.maxBytesBilled);
3
// → "BigQueryのスキャンバイト上限(10GB)を超過します。期間を短縮するか..."
4
}

SQLインジェクション対策

@google-cloud/bigquery のパラメータ化クエリを使い、ユーザー入力を直接SQLに埋め込まない設計にしています。

1
// BAD: 文字列結合(インジェクション可能)
2
const sql = `WHERE page_location LIKE '%${targetPath}%'`;
3
4
// GOOD: パラメータ化クエリ
5
const sql = `WHERE page_location LIKE CONCAT('%', @target_path, '%')`;
6
await client.query(sql, { target_path: targetPath });

4. GA4 BigQuery Exportのスキーマを扱う上での注意点

GA4のBigQueryスキーマは独特です。event_paramsARRAY<STRUCT<key STRING, value STRUCT<...>>> という入れ子構造になっています。

1
-- page_location を取得するには UNNEST が必要
2
SELECT
3
(SELECT ep.value.string_value
4
FROM UNNEST(event_params) AS ep
5
WHERE ep.key = 'page_location') AS page_location
6
FROM `project.dataset.events_*`
7
WHERE _TABLE_SUFFIX BETWEEN @start_suffix AND @end_suffix

また、ga_session_id もこの event_params の中に入っています。セッションを結合する際は必ず user_pseudo_id + ga_session_id の複合キーを使う必要があります。

1
-- ユーザーとセッションを正確に特定する
2
CONCAT(user_pseudo_id, '-', CAST(
3
(SELECT ep.value.int_value FROM UNNEST(event_params) AS ep
4
WHERE ep.key = 'ga_session_id')
5
AS STRING
6
)) AS session_key

コスト管理のために _TABLE_SUFFIX BETWEEN @start_suffix AND @end_suffix全クエリに必ず含めるのも重要です。これがないとテーブル全件スキャンになります。


5. BehaviorFlowAnalyzer — 5軸分析の設計

並列実行

5軸の分析クエリは互いに独立しているため、Promise.all で並列実行しています。

1
const [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(最も近いもの)を取り出します。

1
WITH target_hits AS (
2
SELECT user_pseudo_id, session_id, event_timestamp
3
FROM `project.dataset.events_*`
4
WHERE _TABLE_SUFFIX BETWEEN @start_suffix AND @end_suffix
5
AND event_name = 'page_view'
6
AND page_location LIKE CONCAT('%', @target_path, '%')
7
),
8
prev_pages AS (
9
SELECT
10
REGEXP_REPLACE(page_location, r'^https?://[^/]+', '') AS path,
11
ROW_NUMBER() OVER (
12
PARTITION BY user_pseudo_id, session_id
13
ORDER BY event_timestamp DESC -- 対象ページに最も近い順
14
) AS rn
15
FROM `project.dataset.events_*` AS e
12 collapsed lines
16
JOIN target_hits AS t
17
ON e.user_pseudo_id = t.user_pseudo_id
18
AND e.session_id = t.session_id
19
AND e.event_timestamp < t.event_timestamp -- 対象ページより前
20
WHERE event_name = 'page_view'
21
)
22
SELECT path, COUNT(*) AS sessions
23
FROM prev_pages
24
WHERE rn = 1 -- 直前ページのみ
25
GROUP BY path
26
ORDER BY sessions DESC
27
LIMIT @top_n

セッション完全パスのSQLパターン

ARRAY_AGG(... ORDER BY event_timestamp) でセッション内のページ遷移を配列にまとめます。

1
SELECT
2
ARRAY_AGG(
3
REGEXP_REPLACE(page_location, r'^https?://[^/]+', '')
4
ORDER BY event_timestamp
5
) AS page_sequence,
6
COUNT(*) AS sessions
7
FROM session_pages
8
GROUP BY ARRAY_TO_STRING(page_sequence, ' > ')
9
ORDER BY sessions DESC
10
LIMIT @top_n

これにより ["/top/", "/leasing/", "/contact/"] のような遷移パターンを集計できます。


6. MCPツール層 — 既存アーキテクチャへの追加方法

MCPツールの追加は3ステップです。既存パターンに合わせた実装なので、迷う箇所はありませんでした。

Step 1: Zodスキーマ定義src/mcp/utils/validator.ts に追記)

1
export 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 を新規作成)

1
async 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行追加)

1
return [
2
createAnalyzeExploratoryTool(AnalyzeExploratoryInputSchema),
3
createAnalyzeComparisonTool(AnalyzeComparisonInputSchema),
4
createCheckConfigTool(CheckConfigInputSchema),
5
createValidateConfigTool(ValidateConfigInputSchema),
6
createAnalyzeBehaviorFlowTool(AnalyzeBehaviorFlowInputSchema), // ← 追加
7
];

7. エラー処理の設計 — 継承を活かしたマッピング

BigQuery固有のエラークラスは既存のエラー階層に組み込みました。

1
GA4AnalyzerError
2
├── ConfigurationError
3
│ └── BigQueryNotConfiguredError ← BQ環境変数未設定
4
├── ApiError
5
│ ├── QuotaExceededError
6
│ │ └── BigQueryCostLimitError ← スキャンバイト超過
7
│ └── BigQueryError ← BQ一般エラー

toMCPError() では具体的なクラスを先にチェックすることで、汎用クラスへの誤マッチを防いでいます。

1
// NG: ConfigurationError を先にチェックすると BigQueryNotConfiguredError が
2
// ここに引っかかり、BigQuery専用のメッセージが返らない
3
if (error instanceof ConfigurationError) { ... } // 汎用
4
if (error instanceof BigQueryNotConfiguredError) { ... } // 具体的
5
6
// OK: 具体的なものから先にチェック
7
if (error instanceof BigQueryNotConfiguredError) { ... } // 具体的
8
if (error instanceof BigQueryCostLimitError) { ... } // 具体的
9
if (error instanceof BigQueryError) { ... } // 中間
10
if (error instanceof ConfigurationError) { ... } // 汎用(フォールバック)

8. テスト戦略 — BigQueryのモック

@google-cloud/bigqueryBigQuery クラスをモックする際、vitest 4.x では function キーワードが必要です。

1
// vitest 4.x での class mock
2
const mockDatasetExists = vi.fn().mockResolvedValue([true]);
3
const mockBigQueryQuery = vi.fn().mockResolvedValue([[{ result: 1 }]]);
4
5
vi.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
3
analyze_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
}

まとめ

今回の実装を通じて得た知見をまとめます。

学んだこと

  1. GA4のBigQueryスキーマは UNNEST が要るevent_params の入れ子構造をあらかじめ把握してから実装を始めることが重要。INFORMATION_SCHEMA.COLUMNS でスキーマを確認してから設計するのが確実。

  2. maximumBytesBilled は必須 — コスト管理なしでBigQueryを使うのは危険。デフォルト10GBを設定し、超過時は詳細なエラーメッセージを返す。

  3. vitest 4.x での class mock は function キーワード — arrow functionでコンストラクタをモックするとwarningが出て挙動が不安定になる。

  4. エラー継承チェックは具体的なものからinstanceof チェックの順序で親クラスのマッチングを防ぐ。

  5. MCPツール追加は3ステップ — Zodスキーマ → ハンドラー実装 → 登録。既存パターンに倣えばスムーズ。

Article title:GA4 MCPサーバーにBigQuery Exportを統合して「ユーザー行動フロー分析」を追加して自然言語で分析できるようにした
Article author:45395
Release time:2026-03-10