# 機能設計書 22-メール分析

## 概要

本ドキュメントは、Ghostのメール配信に関する分析機能の設計仕様を記述する。メール分析機能は、ニュースレターの配信結果（開封率・クリック率・バウンス率等）を収集・集計し、パブリッシャーがコンテンツの効果を測定できるようにする。

### 本機能の処理概要

**業務上の目的・背景**：ニュースレター運営において、配信したメールの効果測定は購読者エンゲージメントを把握し、コンテンツ戦略を改善するための重要な指標となる。メール分析機能は、Mailgunから取得したイベントデータを処理・集計し、開封率・クリック率・配信成功率などの指標をダッシュボードに表示することで、パブリッシャーの意思決定を支援する。

**機能の利用シーン**：
- ニュースレター配信後のパフォーマンス確認
- 購読者のエンゲージメント分析
- 配信問題（バウンス・スパム報告）の検出と対応
- メンバー個人の購読状態確認

**主要な処理内容**：
1. Mailgun APIからのイベントデータフェッチ（delivered, opened, failed, unsubscribed, complained）
2. イベントデータの処理・保存
3. メール単位の統計集計（配信数、開封数、失敗数）
4. メンバー単位の統計集計（受信数、開封数、開封率）
5. バックグラウンドジョブによる定期的なデータ同期

**関連システム・外部連携**：
- Mailgun API（イベントデータ取得元）
- バックグラウンドジョブスケジューラ

**権限による制御**：管理者・編集者がダッシュボードで統計を閲覧可能。メンバーは自身の購読情報のみ参照可能。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 25 | ニュースレター分析画面 | 主機能 | ニュースレターの開封率・クリック率分析 |
| 30 | 投稿ニュースレター分析画面 | 主機能 | 投稿のニュースレター配信パフォーマンス分析 |

## 機能種別

データ収集 / 集計処理 / バッチ処理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| begin | Date | Yes | イベント取得開始日時 | end より前であること |
| end | Date | Yes | イベント取得終了日時 | begin より後であること |
| maxEvents | number | No | 最大取得イベント数 | 正の整数 |
| eventTypes | string[] | No | 取得するイベント種別 | delivered, opened, failed, unsubscribed, complained |

### 入力データソース

- **Mailgun Events API**: イベントデータ（配信、開封、失敗、購読解除、スパム報告）
- **emails テーブル**: 配信済みメール情報
- **email_recipients テーブル**: メール受信者情報
- **members テーブル**: メンバー統計情報

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| eventCount | number | 処理したイベント総数 |
| apiPollingTimeMs | number | API呼び出し時間（ミリ秒） |
| processingTimeMs | number | イベント処理時間（ミリ秒） |
| aggregationTimeMs | number | 統計集計時間（ミリ秒） |
| result.delivered | number | 配信成功数 |
| result.opened | number | 開封数 |
| result.permanentFailed | number | 永続的失敗数 |
| result.temporaryFailed | number | 一時的失敗数 |
| result.unsubscribed | number | 購読解除数 |
| result.complained | number | スパム報告数 |

### 出力先

- **emails テーブル**: delivered_count, opened_count, failed_count の更新
- **email_recipients テーブル**: delivered_at, opened_at, failed_at の更新
- **members テーブル**: email_count, email_opened_count, email_open_rate の更新
- **jobs テーブル**: ジョブ実行状態の記録

## 処理フロー

### 処理シーケンス

```
1. フェッチジョブの初期化
   └─ 前回実行時のタイムスタンプを取得
   └─ 取得範囲（begin, end）を決定

2. Mailgun APIからイベントフェッチ
   └─ ページネーション対応で全イベントを取得
   └─ バッチ単位（300件）で処理

3. イベントの処理
   └─ イベント種別に応じた処理を実行
   └─ email_recipients テーブルの更新
   └─ 処理結果の集計

4. 統計の集計
   └─ 5分ごと、または5000メンバーごとに中間集計
   └─ emails テーブルの統計更新
   └─ members テーブルの統計更新

5. ジョブ完了処理
   └─ 最終タイムスタンプの保存
   └─ ジョブステータスの更新
```

### フローチャート

```mermaid
flowchart TD
    A[開始] --> B[前回タイムスタンプ取得]
    B --> C[取得範囲決定]
    C --> D{end <= begin?}
    D -->|Yes| E[スキップ]
    D -->|No| F[Mailgun API呼び出し]
    F --> G[イベントバッチ取得]
    G --> H{イベントあり?}
    H -->|No| I[最終集計]
    H -->|Yes| J[イベント処理]
    J --> K{イベント種別}
    K -->|delivered| L[handleDelivered]
    K -->|opened| M[handleOpened]
    K -->|failed| N[handleFailed]
    K -->|unsubscribed| O[handleUnsubscribed]
    K -->|complained| P[handleComplained]
    L --> Q[結果マージ]
    M --> Q
    N --> Q
    O --> Q
    P --> Q
    Q --> R{5分経過 or 5000件?}
    R -->|Yes| S[中間集計実行]
    R -->|No| T{次のバッチあり?}
    S --> T
    T -->|Yes| G
    T -->|No| I
    I --> U[タイムスタンプ保存]
    U --> V[終了]
    E --> V
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-22-01 | 信頼閾値 | 30分以上前のイベントのみを信頼して処理（TRUST_THRESHOLD_MS） | fetchMissing 処理時 |
| BR-22-02 | 終了マージン | 1分以内のイベントは次回フェッチに回す（FETCH_LATEST_END_MARGIN_MS） | fetchLatest 処理時 |
| BR-22-03 | 開封・非開封分離 | 開封イベントと非開封イベントは別ジョブで処理 | パフォーマンス最適化 |
| BR-22-04 | 重複処理防止 | 同一イベントIDは再処理しない | イベント処理時 |

### 計算ロジック

**開封率の計算**：
```
email_open_rate = (email_opened_count / email_count) * 100
```
※ email_countが0の場合はnull

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| イベント処理 | email_recipients | UPDATE | delivered_at, opened_at, failed_at の更新 |
| イベント処理 | email_recipient_failures | INSERT | 失敗情報の記録 |
| 統計集計 | emails | UPDATE | delivered_count, opened_count, failed_count の更新 |
| 統計集計 | members | UPDATE | email_count, email_opened_count, email_open_rate の更新 |
| ジョブ管理 | jobs | UPDATE | ジョブ実行状態の記録 |

### テーブル別操作詳細

#### email_recipients

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | delivered_at | イベントタイムスタンプ | 配信成功時 |
| UPDATE | opened_at | イベントタイムスタンプ | 開封時 |
| UPDATE | failed_at | イベントタイムスタンプ | 失敗時 |

#### emails

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | delivered_count | COUNT(delivered_at IS NOT NULL) | 集計処理 |
| UPDATE | opened_count | COUNT(opened_at IS NOT NULL) | 集計処理 |
| UPDATE | failed_count | COUNT(failed_at IS NOT NULL) | 集計処理 |

#### members

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | email_count | 受信メール総数 | 集計処理 |
| UPDATE | email_opened_count | 開封メール総数 | 集計処理 |
| UPDATE | email_open_rate | 開封率（%） | 集計処理 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | ValidationError | スケジュール済みフェッチ実行中に新規スケジュール | エラーメッセージを返却 |
| - | InternalServerError | フェッチキャンセル | 処理を中断し、部分結果を保存 |
| - | API Error | Mailgun API呼び出し失敗 | ログ出力し、次回リトライ |

### リトライ仕様

- API呼び出し失敗時は次回ジョブ実行時に自動リトライ
- 処理中断時は最後に成功したタイムスタンプから再開

## トランザクション仕様

- イベント処理は個別トランザクション
- 集計処理はバッチ単位でトランザクション管理
- 中間集計失敗時もフェッチ処理は継続

## パフォーマンス要件

- バッチサイズ: 300イベント/リクエスト
- 中間集計間隔: 5分または5000メンバー
- メンバー統計のバッチ処理: 100メンバー/バッチ

## セキュリティ考慮事項

- Mailgun APIキーはサーバーサイドでのみ使用
- メンバーの個人情報はログに出力しない
- 統計データへのアクセスは管理者権限で制限

## 備考

- 開封追跡はトラッキングピクセルを使用（プライバシー設定で無効化可能）
- Prometheusメトリクスで集計処理の監視が可能

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | event-processing-result.js | `ghost/core/core/server/services/email-analytics/event-processing-result.js` | イベント処理結果を保持するクラス構造 |
| 1-2 | schema.js | `ghost/core/core/server/data/schema/schema.js` | emails(826-868行), email_recipients(889-906行), members(417-443行)のスキーマ |

**読解のコツ**: EventProcessingResultクラスはイベント種別ごとのカウントとemailIds/memberIdsを保持。このデータ構造が集計処理の基盤となる。

#### Step 2: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | email-analytics-service.js | `ghost/core/core/server/services/email-analytics/email-analytics-service.js` | EmailAnalyticsServiceクラスがメインのサービスクラス |

**主要処理フロー**:
1. **153-163行目**: fetchLatestOpenedEvents() - 開封イベントの取得
2. **172-183行目**: fetchLatestNonOpenedEvents() - 非開封イベントの取得
3. **191-209行目**: fetchMissing() - 欠損イベントの取得
4. **314-477行目**: #fetchEvents() - 実際のフェッチ処理
5. **533-618行目**: processEvent() - イベント種別ごとの処理分岐
6. **625-652行目**: aggregateStats() - 統計集計処理

#### Step 3: プロバイダー実装を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | email-analytics-provider-mailgun.js | `ghost/core/core/server/services/email-analytics/email-analytics-provider-mailgun.js` | Mailgun APIとの連携実装 |

**主要処理フロー**:
- **31-44行目**: fetchLatest() - Mailgun APIのオプション設定とフェッチ呼び出し
- **DEFAULT_EVENT_FILTER**: 取得するイベント種別の定義

#### Step 4: クエリ実装を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | queries.js | `ghost/core/core/server/services/email-analytics/lib/queries.js` | データベースクエリ実装 |

**主要処理フロー**:
- aggregateEmailStats(): メール単位の統計集計SQL
- aggregateMemberStats(): メンバー単位の統計集計SQL
- aggregateMemberStatsBatch(): バッチ単位のメンバー統計集計

### プログラム呼び出し階層図

```
EmailAnalyticsService
    │
    ├─ fetchLatestOpenedEvents()
    │      └─ #fetchEvents()
    │             ├─ MailgunProvider.fetchLatest()
    │             │      └─ MailgunClient.fetchEvents()
    │             │
    │             ├─ processEventBatch()
    │             │      └─ processEvent()
    │             │             ├─ handleDelivered()
    │             │             ├─ handleOpened()
    │             │             ├─ handlePermanentFailed()
    │             │             ├─ handleTemporaryFailed()
    │             │             ├─ handleUnsubscribed()
    │             │             └─ handleComplained()
    │             │
    │             └─ aggregateStats()
    │                    ├─ aggregateEmailStats()
    │                    └─ aggregateMemberStats()
    │
    ├─ fetchLatestNonOpenedEvents()
    │      └─ (同上)
    │
    └─ fetchMissing()
           └─ (同上)
```

### データフロー図

```
[入力]                     [処理]                          [出力]

Mailgun Events API ──────▶ fetchLatest() ──────────────▶ Raw Events
                                │
Raw Events ──────────────▶ processEventBatch() ────────▶ EventProcessingResult
                                │
                                ├─ handleDelivered()
                                ├─ handleOpened()
                                ├─ handleFailed()
                                ├─ handleUnsubscribed()
                                └─ handleComplained()
                                │
EventProcessingResult ───▶ aggregateStats() ───────────▶ DB Updates
                                │
                                ├─ aggregateEmailStats() ──▶ emails.delivered_count
                                │                           emails.opened_count
                                │                           emails.failed_count
                                │
                                └─ aggregateMemberStats() ─▶ members.email_count
                                                            members.email_opened_count
                                                            members.email_open_rate
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| email-analytics-service.js | `ghost/core/core/server/services/email-analytics/email-analytics-service.js` | ソース | メインのサービスクラス |
| email-analytics-provider-mailgun.js | `ghost/core/core/server/services/email-analytics/email-analytics-provider-mailgun.js` | ソース | Mailgun API連携 |
| event-processing-result.js | `ghost/core/core/server/services/email-analytics/event-processing-result.js` | ソース | 処理結果データクラス |
| queries.js | `ghost/core/core/server/services/email-analytics/lib/queries.js` | ソース | DBクエリ実装 |
| email-analytics-service-wrapper.js | `ghost/core/core/server/services/email-analytics/email-analytics-service-wrapper.js` | ソース | サービスラッパー |
| index.js | `ghost/core/core/server/services/email-analytics/index.js` | ソース | モジュールエントリーポイント |
| fetch-latest/index.js | `ghost/core/core/server/services/email-analytics/jobs/fetch-latest/index.js` | ソース | フェッチジョブ |
| update-member-email-analytics/index.js | `ghost/core/core/server/services/email-analytics/jobs/update-member-email-analytics/index.js` | ソース | メンバー統計更新ジョブ |
| schema.js | `ghost/core/core/server/data/schema/schema.js` | 設定 | DBスキーマ定義 |
