# 機能設計書 52-記事分析

## 概要

本ドキュメントは、Ghostの記事分析機能に関する設計を記述します。この機能は、個別の記事（投稿）に対するビュー数、エンゲージメント、メンバー獲得、ニュースレター配信パフォーマンスなどの詳細な分析情報を提供します。

### 本機能の処理概要

**業務上の目的・背景**：コンテンツクリエイターが個々の記事のパフォーマンスを詳細に把握し、どのコンテンツがオーディエンスに響いているかを分析するために必要な機能です。記事単位での成功パターンを特定し、今後のコンテンツ戦略に活かすことができます。また、メンバーシップビジネスにおいては、どの記事が新規会員獲得に貢献しているかを把握することで、効果的なコンテンツ制作の指針を得られます。

**機能の利用シーン**：記事公開後のパフォーマンス確認、A/Bテスト結果の分析、ニュースレター配信後の効果測定、特定期間のコンテンツパフォーマンス比較、高パフォーマンス記事の特徴分析などの場面で活用されます。

**主要な処理内容**：
1. 投稿別の訪問者数・ページビューの取得（Tinybird連携）
2. 投稿からのメンバー獲得数（無料/有料）の集計
3. 投稿に起因するMRR（月次経常収益）インパクトの算出
4. リファラー（流入元）ソース別の統計取得
5. ニュースレター配信統計（送信数、開封率、クリック率）の取得
6. オーディエンスフィードバック（いいね/悪いね）の集計
7. トップリンク（クリック数上位のリンク）の取得

**関連システム・外部連携**：
- Tinybird: リアルタイム訪問者データ、ページビューデータの取得
- Stripe: 有料コンバージョンに関連する決済データ
- メール配信システム: 開封・クリックトラッキングデータ

**権限による制御**：記事分析データへのアクセスは、`posts` ドキュメントの `browse` 権限を持つユーザーに限定されます。通常、Editor以上のロールが必要です。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 26 | 投稿分析画面 | 主画面 | 個別投稿のアナリティクス表示 |
| 27 | 投稿分析概要画面 | 主画面 | 投稿のオーバービュー・主要指標表示 |
| 28 | 投稿Webトラフィック画面 | 参照画面 | 投稿へのWebトラフィック詳細分析 |
| 29 | 投稿成長分析画面 | 参照画面 | 投稿からのメンバー獲得分析 |
| 30 | 投稿ニュースレター分析画面 | 参照画面 | 投稿のニュースレター配信パフォーマンス分析 |
| 11 | 投稿一覧画面 | 遷移元画面 | 記事ごとの閲覧数・エンゲージメント表示 |

## 機能種別

データ取得 / 計算処理 / 集計処理 / 分析ダッシュボード表示

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | string | Yes | 投稿ID | 有効なUUID形式 |
| date_from | string | No | 集計開始日（YYYY-MM-DD形式） | ISO 8601日付形式 |
| date_to | string | No | 集計終了日（YYYY-MM-DD形式） | ISO 8601日付形式 |
| timezone | string | No | 日付解釈に使用するタイムゾーン | IANA タイムゾーン形式 |
| order | string | No | ソート順（例: 'free_members desc'） | 許可されたフィールドとdesc/asc |
| limit | number | No | 取得件数の上限（デフォルト: 20） | 正の整数 |

### 入力データソース

- データベース: `posts`, `members_created_events`, `members_subscription_created_events`, `members_paid_subscription_events`, `emails`, `redirects`, `members_click_events`, `members_feedback`
- 外部API: Tinybird Analytics API（api_kpis, api_top_sources）
- 投稿データ: Content API経由で取得

## 出力仕様

### 出力データ

#### 投稿成長統計レスポンス

| 項目名 | 型 | 説明 |
|--------|-----|------|
| post_id | string | 投稿ID |
| free_members | number | 無料メンバー獲得数 |
| paid_members | number | 有料メンバー獲得数 |
| mrr | number | MRRインパクト（セント単位） |

#### リファラー統計レスポンス

| 項目名 | 型 | 説明 |
|--------|-----|------|
| source | string | 流入元ソース名 |
| referrer_url | string | リファラーURL |
| free_members | number | 無料メンバー獲得数 |
| paid_members | number | 有料メンバー獲得数 |
| mrr | number | MRRインパクト |

#### ニュースレター統計レスポンス

| 項目名 | 型 | 説明 |
|--------|-----|------|
| sent | number | 送信数 |
| opened | number | 開封数 |
| clicked | number | クリック数 |
| openedRate | number | 開封率（0-1） |
| clickedRate | number | クリック率（0-1） |

### 出力先

- Admin API経由でフロントエンド（React Posts App）へJSON形式で返却
- キャッシュ: statsService.cacheによる短期キャッシュ

## 処理フロー

### 処理シーケンス

```
1. 投稿分析画面表示リクエスト
   └─ postIdをURLパラメータから取得

2. 投稿データ取得
   └─ getPost APIで投稿の基本情報を取得

3. Web統計取得（Tinybird）
   └─ api_kpis: 訪問者数、ページビュー
   └─ api_top_sources: トップ流入元

4. 成長統計取得（Database）
   └─ postGrowthStats: メンバー獲得数、MRR
   └─ postReferrers: ソース別統計

5. ニュースレター統計取得
   └─ post.email: 送信数、開封数
   └─ post.count.clicks: クリック数
   └─ newsletterBasicStats: 過去のニュースレター統計
   └─ topLinks: クリック上位リンク

6. データ整形・計算
   └─ 開封率・クリック率の計算
   └─ 平均値の計算
   └─ 通貨フォーマット

7. UIレンダリング
   └─ KPIカード、チャート、テーブルの表示
```

### フローチャート

```mermaid
flowchart TD
    A[投稿分析画面アクセス] --> B[postId取得]
    B --> C[投稿データ取得]
    C --> D{投稿存在?}
    D -->|No| E[エラー表示]
    D -->|Yes| F{Web分析有効?}
    F -->|Yes| G[Tinybird統計取得]
    F -->|No| H[成長統計のみ]
    G --> I{ニュースレター送信済?}
    H --> I
    I -->|Yes| J[ニュースレター統計取得]
    I -->|No| K[Web/成長統計のみ表示]
    J --> L[データ整形]
    K --> L
    L --> M[UI表示]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-52-001 | 集計期間自動設定 | 投稿公開日から現在までを自動で集計期間とする | date_from/date_toが未指定の場合 |
| BR-52-002 | 無料メンバー定義 | 投稿で登録したが同じ投稿で有料化しなかったメンバー | free_members集計時 |
| BR-52-003 | 有料メンバー定義 | 投稿に帰属する有料コンバージョンが発生したメンバー | paid_members集計時 |
| BR-52-004 | MRR帰属 | 有料コンバージョン時のMRRデルタを投稿に帰属 | MRR計算時 |
| BR-52-005 | ソース正規化 | リファラーソースは正規化マップで統一表示 | 全ソースデータ |
| BR-52-006 | メールのみ投稿 | email_only投稿はWeb統計を表示しない | 投稿タイプ判定時 |

### 計算ロジック

**開封率計算**:
```
open_rate = opened_count / email_count
```

**クリック率計算**:
```
click_rate = click_count / email_count
```

**平均開封率/クリック率計算**:
```
average_open_rate = SUM(open_rate) / COUNT(newsletters)
average_click_rate = SUM(click_rate) / COUNT(newsletters)
```

**MRRインパクト計算**:
```
mrr = SUM(mrr_delta) WHERE subscription_created_event.attribution_id = post_id
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 投稿取得 | posts | SELECT | 投稿の基本情報を取得 |
| メール統計取得 | emails | SELECT | 送信数・開封数を取得 |
| 無料メンバー集計 | members_created_events | SELECT | 投稿帰属の登録イベントを集計 |
| 有料メンバー集計 | members_subscription_created_events | SELECT | 投稿帰属のコンバージョンを集計 |
| MRR集計 | members_paid_subscription_events | SELECT | MRRデルタを集計 |
| クリック集計 | redirects, members_click_events | SELECT | リンククリック数を集計 |
| フィードバック集計 | members_feedback | SELECT | いいね/悪いね数を集計 |

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

#### members_created_events

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | member_id, attribution_id, referrer_source | attribution_id = postId AND attribution_type IN ('post', 'page') | 重複除外のためDISTINCT |

#### members_subscription_created_events

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | member_id, subscription_id, referrer_source | attribution_id = postId AND attribution_type IN ('post', 'page') | 有料コンバージョン集計 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 400 | BadRequestError | 無効なpost_id形式 | 有効なUUID形式を使用するよう通知 |
| 404 | NotFoundError | 存在しない投稿ID | 投稿が見つからない旨を表示 |
| 403 | PermissionError | 権限不足 | 適切な権限を持つユーザーでの操作を要求 |
| 500 | InternalError | Tinybird接続エラー | DB統計のみで応答、エラーログ出力 |

### リトライ仕様

Tinybird APIへの接続失敗時は、Web統計セクションを非表示にし、データベースベースの成長統計のみを表示します。

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

全て読み取り専用（SELECT）操作のため、トランザクション管理は不要です。

## パフォーマンス要件

- 投稿分析画面の初期表示: 2秒以内
- キャッシュヒット時: 100ms以内
- リファラー統計取得: 1秒以内

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

- 投稿IDの所有権検証（自サイトの投稿のみアクセス可能）
- APIキーの適切な管理（Tinybird連携用）
- 個人を特定できる情報は集計データに含めない

## 備考

- Web統計はTinybirdが設定されている場合のみ表示
- email_only投稿はGrowthタブにリダイレクト
- フロントエンド（apps/posts）は React + Vite で構築

---

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

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

### 推奨読解順序

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

まず、記事分析で使用されるデータ型を理解します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | posts-stats-service.js | `ghost/core/core/server/services/stats/posts-stats-service.js` | ReferrerStatsResult, TopPostResult型定義（66-79行目） |
| 1-2 | kpi-helpers.ts | `apps/posts/src/utils/kpi-helpers.ts` | KpiDataItem型定義 |
| 1-3 | post-analytics-context.tsx | `apps/posts/src/providers/post-analytics-context.tsx` | Post型、GlobalData型 |

**読解のコツ**: TypeScriptの型定義とJSDocコメントの両方を確認することで、データ構造の全体像を把握できます。

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

処理の起点となるコンポーネントとAPIを特定します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | post-analytics.tsx | `apps/posts/src/views/PostAnalytics/post-analytics.tsx` | 投稿分析のルートコンポーネント |
| 2-2 | stats.js | `ghost/core/core/server/api/endpoints/stats.js` | postReferrers, postGrowthStats API（318-388行目） |

**主要処理フロー**:
1. **stats.js 318-356行目**: `postReferrers` - リファラー統計取得
2. **stats.js 357-388行目**: `postGrowthStats` - 成長統計取得
3. **stats.js 389-420行目**: `postStats` - 基本統計取得

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

統計データの集計ロジックを確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | posts-stats-service.js | `ghost/core/core/server/services/stats/posts-stats-service.js` | getReferrersForPost, getGrowthStatsForPost |

**主要処理フロー**:
- **288-398行目**: `getReferrersForPost()` - リファラー別統計の集計
- **400-440行目**: `getGrowthStatsForPost()` - 成長統計の集計
- **1116-1180行目**: `getPostStats()` - 基本統計の取得

#### Step 4: フロントエンドフックを理解する

React製のPosts Appのデータ取得ロジックを確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | use-post-referrers.ts | `apps/posts/src/hooks/use-post-referrers.ts` | リファラー統計フック |
| 4-2 | use-post-newsletter-stats.ts | `apps/posts/src/hooks/use-post-newsletter-stats.ts` | ニュースレター統計フック |

**主要処理フロー**:
- **use-post-referrers.ts 5-63行目**: リファラー統計と通貨情報の取得
- **use-post-newsletter-stats.ts 14-183行目**: ニュースレター統計、フィードバック、トップリンクの取得

#### Step 5: ビューコンポーネントを理解する

画面表示ロジックを確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | overview.tsx | `apps/posts/src/views/PostAnalytics/Overview/overview.tsx` | 概要画面 |
| 5-2 | growth.tsx | `apps/posts/src/views/PostAnalytics/Growth/growth.tsx` | 成長分析画面 |
| 5-3 | newsletter.tsx | `apps/posts/src/views/PostAnalytics/Newsletter/newsletter.tsx` | ニュースレター分析画面 |

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

```
PostAnalytics Component
    │
    ├─ Overview.tsx
    │      │
    │      ├─ useGlobalData()
    │      │      └─ post, statsConfig取得
    │      │
    │      ├─ usePostReferrers(postId)
    │      │      ├─ usePostReferrersAPI()
    │      │      │      └─ GET /stats/:id/referrers/
    │      │      ├─ usePostGrowthStatsAPI()
    │      │      │      └─ GET /stats/:id/growth/
    │      │      └─ useMrrHistory()
    │      │
    │      └─ useTinybirdQuery()
    │             ├─ api_kpis
    │             └─ api_top_sources
    │
    ├─ Growth.tsx
    │      └─ usePostReferrers(postId)
    │
    └─ Newsletter.tsx
           └─ usePostNewsletterStats(postId)
                  ├─ getPost()
                  ├─ useNewsletterBasicStats()
                  ├─ useNewsletterClickStats()
                  └─ useTopLinks()
```

### データフロー図

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

postId            PostsStatsService                  投稿分析データ
       ───▶       ├─ getReferrersForPost()      ───▶  {
                  │    ├─ members_created_events       totals: {
                  │    └─ members_subscription_        free_members,
                  │        created_events              paid_members,
                  ├─ getGrowthStatsForPost()           mrr
                  │    └─ JOIN with                  },
                  │        mrr_delta                   referrers: [...],
                  └─ getPostStats()                    stats: {...}
                       ├─ posts                      }
                       └─ emails

TinybirdQuery            Newsletter Stats
├─ api_kpis        ───▶  ├─ basicStats
└─ api_top_sources       └─ clickStats
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| stats.js | `ghost/core/core/server/api/endpoints/stats.js` | ソース | 統計API エンドポイント |
| posts-stats-service.js | `ghost/core/core/server/services/stats/posts-stats-service.js` | ソース | 投稿統計サービス |
| referrers-stats-service.js | `ghost/core/core/server/services/stats/referrers-stats-service.js` | ソース | リファラー統計サービス |
| app.tsx | `apps/posts/src/app.tsx` | ソース | Posts App ルート |
| routes.tsx | `apps/posts/src/routes.tsx` | ソース | ルーティング定義 |
| post-analytics.tsx | `apps/posts/src/views/PostAnalytics/post-analytics.tsx` | ソース | 分析ルートコンポーネント |
| overview.tsx | `apps/posts/src/views/PostAnalytics/Overview/overview.tsx` | ソース | 概要画面 |
| growth.tsx | `apps/posts/src/views/PostAnalytics/Growth/growth.tsx` | ソース | 成長分析画面 |
| newsletter.tsx | `apps/posts/src/views/PostAnalytics/Newsletter/newsletter.tsx` | ソース | ニュースレター分析画面 |
| web.tsx | `apps/posts/src/views/PostAnalytics/Web/web.tsx` | ソース | Web分析画面 |
| use-post-referrers.ts | `apps/posts/src/hooks/use-post-referrers.ts` | ソース | リファラー統計フック |
| use-post-newsletter-stats.ts | `apps/posts/src/hooks/use-post-newsletter-stats.ts` | ソース | ニュースレター統計フック |
| post-analytics-context.tsx | `apps/posts/src/providers/post-analytics-context.tsx` | ソース | グローバルデータプロバイダー |
| kpi-helpers.ts | `apps/posts/src/utils/kpi-helpers.ts` | ソース | KPIヘルパー関数 |
| link-helpers.ts | `apps/posts/src/utils/link-helpers.ts` | ソース | リンク処理ヘルパー |
