# 帳票設計書 13-投稿別統計レポート

## 概要

本ドキュメントは、Ghost CMSにおける投稿別統計レポートの設計仕様を定義する。このレポートは、特定の投稿に関する詳細な統計情報（閲覧数、メール送信・開封統計、会員獲得数など）を、JSON形式でAPIエンドポイント経由で提供する。

### 本帳票の処理概要

本帳票は、個別の投稿がもたらしたパフォーマンス指標を包括的に集約し、コンテンツの効果測定を支援するための詳細統計データを提供する。

**業務上の目的・背景**：コンテンツマーケティングにおいて、各投稿の効果を定量的に評価することは重要である。投稿ごとのメール配信結果、開封率、会員獲得（サインアップ・有料転換）、閲覧数を一元的に把握することで、どのコンテンツが読者エンゲージメントに貢献しているかを分析できる。本レポートはこれらの指標を統合的に提供する。

**帳票の利用シーン**：管理画面で特定の投稿を選択し、その投稿の詳細パフォーマンス統計を確認する際に利用される。投稿エディタやPosts一覧画面から各投稿の統計情報を閲覧することで、コンテンツ戦略の効果検証が可能になる。

**主要な出力内容**：
1. 投稿ID（id）
2. メール送信数（recipient_count）と開封数（opened_count）
3. 開封率（open_rate）
4. 会員獲得数（member_delta）- 無料会員（free_members）と有料会員（paid_members）
5. 閲覧者数（visitors）- Tinybird連携時のみ

**帳票の出力タイミング**：管理画面で投稿別統計APIエンドポイント（`GET /ghost/api/admin/stats/posts/:id/stats`）にアクセスした際にリアルタイムで生成される。

**帳票の利用者**：サイト管理者、コンテンツマーケター、編集者

## 帳票種別

統計レポート / 詳細分析（JSON形式）

## 利用画面

| 画面No | 画面名 | URL/ルーティング | 出力操作 |
|--------|--------|-----------------|---------|
| - | Post Stats API | `GET /ghost/api/admin/stats/posts/:id/stats` | APIリクエスト |
| - | Posts画面 | `/ghost/#/posts/` | 投稿選択時に自動取得 |

## 出力形式

### 基本仕様

| 項目 | 内容 |
|-----|------|
| ファイル形式 | JSON |
| 用紙サイズ | N/A（API出力） |
| 向き | N/A |
| ファイル名 | N/A（HTTPレスポンス） |
| 出力方法 | API経由でJSONレスポンス |
| 文字コード | UTF-8 |

### レスポンス構造

```json
{
  "data": [
    {
      "id": "string",
      "recipient_count": "number|null",
      "opened_count": "number|null",
      "open_rate": "number|null",
      "member_delta": "number",
      "free_members": "number",
      "paid_members": "number",
      "visitors": "number"
    }
  ]
}
```

## 帳票レイアウト

### レイアウト概要

JSON配列形式で特定投稿の統計情報を返却する。data配列には1つのオブジェクトが含まれる。

```
{
  "data": [
    ┌─────────────────────────────────────┐
    │  id: 投稿ID                         │
    │  recipient_count: メール送信数      │
    │  opened_count: 開封数               │
    │  open_rate: 開封率                  │
    │  member_delta: 会員獲得数合計       │
    │  free_members: 無料会員獲得数       │
    │  paid_members: 有料会員獲得数       │
    │  visitors: 閲覧者数                 │
    └─────────────────────────────────────┘
  ]
}
```

### 明細部

| No | 項目名 | 説明 | データ取得元 | 表示形式 | 備考 |
|----|-------|------|-------------|---------|------|
| 1 | id | 投稿ID | posts.id | 文字列（24文字） | パスパラメータで指定 |
| 2 | recipient_count | メール送信数 | emails.email_count | 整数 or null | メール未送信時はnull |
| 3 | opened_count | 開封数 | emails.opened_count | 整数 or null | メール未送信時はnull |
| 4 | open_rate | 開封率 | 計算値 | 小数（%表記） | (opened_count / email_count) * 100 |
| 5 | member_delta | 会員獲得数合計 | 計算値 | 整数 | free_members + paid_members |
| 6 | free_members | 無料会員獲得数 | members_created_events | 整数 | この投稿経由でサインアップした無料会員数 |
| 7 | paid_members | 有料会員獲得数 | members_subscription_created_events | 整数 | この投稿経由で有料転換した会員数 |
| 8 | visitors | 閲覧者数 | Tinybird（api_top_pages） | 整数 | Tinybird未連携時は0 |

## 出力条件

### 抽出条件

| 条件名 | 説明 | 必須 |
|-------|------|-----|
| id | 投稿ID（パスパラメータ） | Yes |

### ソート順

N/A（単一レコード返却）

### 改ページ条件

N/A（JSON形式のため改ページなし）

## データベース参照仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 結合条件 |
|-----------|------|---------|
| posts | 投稿情報の取得 | posts.id = 指定ID |
| emails | メール送信統計 | emails.post_id = posts.id |
| members_created_events | 無料会員獲得数 | attribution_id = posts.id |
| members_subscription_created_events | 有料会員獲得数 | attribution_id = posts.id |

### テーブル別参照項目詳細

#### posts

| 参照項目（カラム名） | 帳票項目との対応 | 取得条件 | 備考 |
|-------------------|----------------|---------|------|
| id | id | id = 指定値 | 主キー |
| uuid | Tinybird検索用 | status = 'published' | |
| published_at | Tinybird日付範囲用 | | |
| status | フィルタ条件 | 'published' | |

#### emails

| 参照項目（カラム名） | 帳票項目との対応 | 取得条件 | 備考 |
|-------------------|----------------|---------|------|
| email_count | recipient_count | post_id = 指定値 | |
| opened_count | opened_count | | |

#### members_created_events

| 参照項目（カラム名） | 帳票項目との対応 | 取得条件 | 備考 |
|-------------------|----------------|---------|------|
| member_id | COUNT DISTINCT → free_members | attribution_id = post_id | |
| attribution_type | フィルタ条件 | IN ('post', 'page') | |

#### members_subscription_created_events

| 参照項目（カラム名） | 帳票項目との対応 | 取得条件 | 備考 |
|-------------------|----------------|---------|------|
| member_id | COUNT DISTINCT → paid_members | attribution_id = post_id | |
| attribution_type | フィルタ条件 | IN ('post', 'page') | |

## 計算仕様

### 計算項目一覧

| 項目名 | 計算式 | 端数処理 | 備考 |
|-------|-------|---------|------|
| open_rate | (opened_count / email_count) * 100 | なし | email_count=0の場合はnull |
| member_delta | free_members + paid_members | なし | 合計値 |
| free_members | COUNT DISTINCT member_id（有料転換していない会員） | なし | LEFT JOIN + WHERE msce.id IS NULL |
| paid_members | COUNT DISTINCT member_id（有料転換した会員） | なし | |

### 会員獲得数計算ロジック

- **free_members**: この投稿経由でサインアップしたが、同じ投稿で有料転換していない会員数
- **paid_members**: この投稿経由で有料転換した会員数
- 両方の合計がmember_deltaとして表示される

## 処理フロー

### 出力フロー

```mermaid
flowchart TD
    A[APIリクエスト受信] --> B{postId チェック}
    B -->|空文字| C[空配列返却]
    B -->|有効| D[投稿情報クエリ]
    D --> E{投稿が存在するか}
    E -->|存在しない| F[空配列返却]
    E -->|存在する| G[会員獲得数クエリ]
    G --> H[開封率計算]
    H --> I{Tinybird連携}
    I -->|有効| J[閲覧者数取得]
    I -->|無効| K[visitors = 0]
    J --> L[結果をJSON形式で返却]
    K --> L
    L --> M[終了]
```

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 表示メッセージ | 対処方法 |
|----------|---------|--------------|---------|
| データなし | postIdが空文字 | N/A | 空配列返却 |
| データなし | 投稿が存在しない or status != 'published' | N/A | 空配列返却 |
| Tinybirdエラー | Tinybird API呼び出し失敗 | ログ出力 | visitors = 0 で続行 |
| 内部エラー | クエリ実行失敗 | ログ出力のみ | 空配列返却 |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定データ件数 | 1件（単一投稿の統計） |
| 目標出力時間 | 1秒以内 |
| 同時出力数上限 | 制限なし（通常のAPI制限に従う） |

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

- Admin API認証が必須（mw.authAdminApi）
- posts権限のbrowseパーミッションが必要
- APIキャッシュによる負荷軽減（statsService.cache）
- 個人を特定できる情報（メールアドレス等）は含まれない

## 備考

- Tinybird連携が有効な場合のみvisitorsが取得される
- 投稿のstatusが'published'でない場合は空配列が返却される
- 会員獲得数は、_getMemberAttributionCountsメソッドで計算される
- 閲覧者数はTinybirdのapi_top_pagesエンドポイントから取得される

---

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

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

### 推奨読解順序

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

まず、投稿別統計で扱うデータ構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | schema.js | `ghost/core/core/server/data/schema/schema.js` | posts, emails, members_created_events, members_subscription_created_eventsテーブルの構造 |

**読解のコツ**: `members_created_events`のattribution_id, attribution_typeカラムと、`members_subscription_created_events`の同様のカラムが投稿への帰属を追跡している。

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

処理の起点となるAPIルーティングとコントローラーを特定。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | routes.js | `ghost/core/core/server/web/api/endpoints/admin/routes.js` | postStatsのルート定義（行162） |
| 2-2 | stats.js | `ghost/core/core/server/api/endpoints/stats.js` | postStatsコントローラー（行389-420） |

**主要処理フロー**:
1. **行162**: `GET /stats/posts/:id/stats`ルート定義
2. **行389-420**: `postStats`コントローラー、idパラメータの検証とサービス呼び出し

#### Step 3: サービス層を理解する

ビジネスロジックの中核となるサービスクラスを読み解く。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | stats-service.js | `ghost/core/core/server/services/stats/stats-service.js` | getPostStatsメソッド（行161-163） |
| 3-2 | posts-stats-service.js | `ghost/core/core/server/services/stats/posts-stats-service.js` | getPostStats実装（行1116-1180） |

**主要処理フロー**:
- **posts-stats-service.js 行1119-1121**: postIdのバリデーション
- **posts-stats-service.js 行1124-1130**: 投稿情報とメール統計の取得
- **posts-stats-service.js 行1137-1138**: 会員獲得数の取得
- **posts-stats-service.js 行1145-1146**: 開封率の計算
- **posts-stats-service.js 行1149-1162**: Tinybird連携時の閲覧者数取得

#### Step 4: 会員獲得数計算を理解する

_getMemberAttributionCountsメソッドの詳細を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | posts-stats-service.js | `ghost/core/core/server/services/stats/posts-stats-service.js` | _getMemberAttributionCountsメソッド（行1401-1466） |

**主要処理フロー**:
- **行1411-1422**: free_membersクエリ（サインアップしたが有料転換していない）
- **行1429-1434**: paid_membersクエリ（有料転換した会員）
- **行1440-1443**: 並列クエリ実行
- **行1446-1455**: 結果の結合

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

```
routes.js (ルート定義)
    │
    └─ stats.js (APIコントローラー)
           │
           └─ postStats
                  └─ statsService.api.getPostStats(postId)
                          └─ StatsService.getPostStats()
                                  └─ PostsStatsService.getPostStats()
                                          │
                                          ├─ 投稿・メール情報クエリ
                                          │      └─ posts LEFT JOIN emails
                                          │
                                          ├─ _getMemberAttributionCounts()
                                          │      ├─ members_created_events
                                          │      └─ members_subscription_created_events
                                          │
                                          └─ Tinybird閲覧者数取得
                                                 └─ api_top_pages
```

### データフロー図

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

HTTPリクエスト ───▶ API Controller ───▶ StatsService ───▶ JSON Response
  (postId)             (パラメータ検証)     (クエリ構築・実行)     { id, recipient_count,
                                                                  opened_count, open_rate,
                                                                  member_delta, free_members,
                                                                  paid_members, visitors }

                              │
                              ▼
                    ┌──────────────────┐
                    │   Database       │
                    │ ├─ posts         │ ──▶ 投稿情報、uuid
                    │ ├─ emails        │ ──▶ 送信数、開封数
                    │ ├─ members_      │
                    │ │   created_     │ ──▶ 無料会員獲得数
                    │ │   events       │
                    │ └─ members_      │
                    │     subscription_│ ──▶ 有料会員獲得数
                    │     created_     │
                    │     events       │
                    └──────────────────┘
                              │
                              ▼
                    ┌──────────────────┐
                    │   Tinybird       │
                    │   api_top_pages  │ ──▶ 閲覧者数
                    └──────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| routes.js | `ghost/core/core/server/web/api/endpoints/admin/routes.js` | ソース | APIルート定義 |
| stats.js | `ghost/core/core/server/api/endpoints/stats.js` | ソース | APIコントローラー |
| stats-service.js | `ghost/core/core/server/services/stats/stats-service.js` | ソース | サービス層ラッパー |
| posts-stats-service.js | `ghost/core/core/server/services/stats/posts-stats-service.js` | ソース | 投稿別統計クエリ実装 |
| tinybird.js | `ghost/core/core/server/services/stats/utils/tinybird.js` | ソース | Tinybirdクライアント |
| schema.js | `ghost/core/core/server/data/schema/schema.js` | 設定 | DBスキーマ定義 |
