# 機能設計書 51-サイト統計

## 概要

本ドキュメントは、Ghostのサイト統計機能に関する設計を記述します。この機能は、サイト全体のトラフィック、メンバー数、MRR（月次経常収益）などの統計情報を集計・表示するダッシュボード機能です。

### 本機能の処理概要

**業務上の目的・背景**：コンテンツパブリッシャーがサイトのパフォーマンスを定量的に把握し、コンテンツ戦略やマーケティング施策の効果を測定するために必要な機能です。特にメンバーシップビジネスを運営するサイトにとって、メンバー数の推移や収益の可視化は経営判断に不可欠な情報となります。

**機能の利用シーン**：サイト管理者や編集者が、日次・週次・月次でサイトの成長を確認する際に利用します。新規コンテンツ公開後のトラフィック変化の確認、マーケティングキャンペーンの効果測定、収益目標の達成度確認などの場面で活用されます。

**主要な処理内容**：
1. メンバー数の履歴データ取得と集計（無料/有料/コンプ別）
2. MRR（月次経常収益）の履歴データ取得と通貨別集計
3. サブスクリプションの登録・解約履歴の取得
4. リファラー（流入元）別の統計データ集計
5. トップコンテンツの閲覧数ランキング生成
6. ニュースレター配信統計（開封率・クリック率）の集計
7. Tinybirdとの連携によるリアルタイム訪問者データの取得

**関連システム・外部連携**：
- Tinybird: リアルタイムアナリティクスデータの取得（訪問者数、ページビューなど）
- Stripe: 決済データとの連携によるMRR算出
- データベース: メンバー・サブスクリプションイベントの永続化

**権限による制御**：統計データへのアクセスは、`members` ドキュメントの `browse` 権限、および `posts` ドキュメントの `browse` 権限を持つユーザーに限定されます。通常、管理者（Administrator）以上のロールが必要です。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 8 | ホーム画面 | 主画面 | サイト全体の統計情報のダッシュボード表示 |
| 21 | 統計（Analytics）画面 | 主画面 | サイト全体の統計分析ダッシュボード |
| 22 | 統計概要画面 | 主画面 | 統計のオーバービュー・主要KPI表示 |
| 23 | Webトラフィック画面 | 参照画面 | Webトラフィックの詳細分析 |
| 24 | 成長分析画面 | 参照画面 | メンバー増減・収益の成長分析 |

## 機能種別

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

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| 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 | ソート順（例: 'signups desc'） | 許可されたフィールドとdesc/asc |
| limit | number | No | 取得件数の上限（デフォルト: 20） | 正の整数であること |
| member_status | string | No | メンバーステータスでのフィルタ | free/paid/comped |
| post_type | string | No | 投稿タイプでのフィルタ | post/page |
| newsletter_id | string | No | ニュースレターID | 有効なUUID形式 |

### 入力データソース

- データベース: `members`, `members_status_events`, `members_subscription_created_events`, `members_paid_subscription_events`, `members_created_events`, `posts`, `emails`, `redirects`, `members_click_events`
- 外部API: Tinybird Analytics API（訪問者データ、ページビューデータ）
- 設定キャッシュ: `web_analytics_enabled` 設定

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| data | array | 統計データの配列 |
| meta | object | メタデータ（合計値など） |
| meta.totals | object | 現在の合計値（paid, free, comped） |

#### メンバー数履歴レスポンス

| 項目名 | 型 | 説明 |
|--------|-----|------|
| date | string | 日付（YYYY-MM-DD形式） |
| paid | number | 有料メンバー数 |
| free | number | 無料メンバー数 |
| comped | number | コンプ（無料提供）メンバー数 |
| paid_subscribed | number | 当日の有料登録数 |
| paid_canceled | number | 当日の有料解約数 |

#### MRR履歴レスポンス

| 項目名 | 型 | 説明 |
|--------|-----|------|
| date | string | 日付（YYYY-MM-DD形式） |
| mrr | number | 月次経常収益（セント単位） |
| currency | string | 通貨コード（例: usd） |

#### トップソースレスポンス

| 項目名 | 型 | 説明 |
|--------|-----|------|
| source | string | 流入元ソース名 |
| signups | number | 無料登録数 |
| paid_conversions | number | 有料コンバージョン数 |
| mrr | number | MRR貢献額 |

### 出力先

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

## 処理フロー

### 処理シーケンス

```
1. API リクエスト受信
   └─ Controller（stats.js）がリクエストを受け取る

2. 権限チェック
   └─ members:browse または posts:browse 権限を確認

3. パラメータ解析
   └─ date_from, date_to, timezone などのオプションを解析

4. キャッシュ確認
   └─ 同一パラメータの結果がキャッシュにあれば返却

5. データ取得・集計
   └─ StatsServiceを通じて各サービスからデータを取得
      ├─ MembersStatsService: メンバー数履歴
      ├─ MrrStatsService: MRR履歴
      ├─ SubscriptionStatsService: サブスクリプション履歴
      ├─ ReferrersStatsService: リファラー統計
      ├─ PostsStatsService: 投稿統計
      ├─ ContentStatsService: コンテンツ統計
      └─ TinybirdClient: 訪問者データ（オプション）

6. データ整形
   └─ 日付範囲での補完、正規化処理

7. レスポンス返却
   └─ JSON形式でフロントエンドに返却
```

### フローチャート

```mermaid
flowchart TD
    A[API リクエスト受信] --> B{権限チェック}
    B -->|権限なし| C[403 Forbidden]
    B -->|権限あり| D{キャッシュ確認}
    D -->|キャッシュヒット| E[キャッシュデータ返却]
    D -->|キャッシュミス| F[パラメータ解析]
    F --> G[日付範囲計算]
    G --> H{Tinybird有効?}
    H -->|Yes| I[Tinybird初期化]
    H -->|No| J[DBのみで集計]
    I --> J
    J --> K[各サービスでデータ取得]
    K --> L[データ集計・整形]
    L --> M[キャッシュ保存]
    M --> N[レスポンス返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-51-001 | デフォルト集計期間 | 日付指定がない場合、過去91日間を対象とする | startDateが未指定の場合 |
| BR-51-002 | 通貨選択 | MRRは最も収益の高い通貨で表示する | 複数通貨がある場合 |
| BR-51-003 | ソース正規化 | リファラーソースは統一された名称に正規化する | 全てのソースデータ |
| BR-51-004 | ゼロ値補完 | データのない日付にはゼロ値を補完する | 日付範囲内の欠損日 |
| BR-51-005 | 将来日付除外 | 将来の日付のデータは結果から除外する | 全ての集計処理 |

### 計算ロジック

**メンバー数計算**:
- 総メンバー数 = 無料メンバー + 有料メンバー + コンプメンバー
- 日次デルタ = 当日の登録数 - 当日の解約数

**MRR計算**:
- MRRデルタ = `members_paid_subscription_events.mrr_delta` の合計
- 累積MRR = 前日のMRR + 当日のMRRデルタ

**オープン率計算**:
```
open_rate = opened_count / email_count
```

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

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| メンバー数取得 | members | SELECT | ステータス別のメンバー数を集計 |
| メンバー履歴取得 | members_status_events | SELECT | 日別のステータス変更を集計 |
| MRR取得 | members_stripe_customers_subscriptions | SELECT | 通貨別の現在MRRを集計 |
| MRR履歴取得 | members_paid_subscription_events | SELECT | 日別のMRR変動を集計 |
| サインアップ取得 | members_created_events | SELECT | 日別・ソース別のサインアップを集計 |
| コンバージョン取得 | members_subscription_created_events | SELECT | 日別・ソース別のコンバージョンを集計 |
| 投稿統計取得 | posts, emails | SELECT | 投稿のメール配信統計を取得 |
| クリック統計取得 | redirects, members_click_events | SELECT | リンククリック数を集計 |

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

#### members

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id, status | GROUP BY status | ステータス別カウント |

#### members_status_events

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | created_at, from_status, to_status | created_at >= startDate | 日別集計 |

#### members_paid_subscription_events

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | created_at, currency, mrr_delta | created_at >= startDate | 通貨別・日別集計 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 400 | BadRequestError | 無効なorderフィールド指定 | 有効なフィールド名をエラーメッセージで通知 |
| 400 | BadRequestError | 無効なorder方向指定 | 'asc' または 'desc' を使用するよう通知 |
| 403 | PermissionError | 権限不足 | 適切な権限を持つユーザーでの操作を要求 |
| 500 | InternalError | DB接続エラー | ログ出力後、空の結果を返却 |
| 500 | InternalError | Tinybird接続エラー | ログ出力後、DB統計のみで応答 |

### リトライ仕様

Tinybird APIへの接続失敗時は、自動的にデータベースのみの統計に切り替えて応答します。リトライは行わず、次回リクエスト時に再度Tinybird接続を試みます。

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

統計データの取得は全て読み取り専用（SELECT）であるため、トランザクション管理は不要です。複数のクエリは並列実行され、それぞれ独立して処理されます。

## パフォーマンス要件

- 統計ダッシュボードの初期表示: 3秒以内
- キャッシュヒット時: 100ms以内
- 日付範囲90日以内での集計: 2秒以内
- キャッシュTTL: 設定可能（デフォルト: 5分）

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

- 統計データへのアクセスは認証済みスタッフユーザーに限定
- APIキーの適切な管理（Tinybird連携用）
- 個人を特定できる情報は集計データに含めない
- レートリミットの適用によるDDoS対策

## 備考

- Tinybird連携は `web_analytics_enabled` 設定が有効な場合のみ動作
- ソース正規化マップは `referrers-stats-service.js` で定義
- フロントエンド（apps/stats）は React + Vite で構築

---

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

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

### 推奨読解順序

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

まず、統計データの型定義と構造を理解することが重要です。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | members-stats-service.js | `ghost/core/core/server/services/stats/members-stats-service.js` | MemberStatusDelta, TotalMembersByStatus 型定義（266-299行目） |
| 1-2 | mrr-stats-service.js | `ghost/core/core/server/services/stats/mrr-stats-service.js` | MrrByCurrency, MrrDelta, MrrHistory 型定義（139-166行目） |
| 1-3 | posts-stats-service.js | `ghost/core/core/server/services/stats/posts-stats-service.js` | TopPostsOptions, AttributionResult 等の型定義（11-79行目） |

**読解のコツ**: JSDocコメントによる型定義が多用されています。`@typedef` で定義された型は、関数の引数・戻り値の理解に不可欠です。

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

処理の起点となるAPIエンドポイントを特定します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | stats.js（API） | `ghost/core/core/server/api/endpoints/stats.js` | 統計APIの全エンドポイント定義 |

**主要処理フロー**:
1. **6-28行目**: `memberCountHistory` - メンバー数履歴の取得
2. **30-53行目**: `mrr` - MRR履歴の取得
3. **94-129行目**: `topContent` - トップコンテンツの取得
4. **495-528行目**: `topSourcesGrowth` - ソース別成長統計の取得

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

統計データの集計ロジックを担当するサービス層を確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | stats-service.js | `ghost/core/core/server/services/stats/stats-service.js` | 統計サービスのファサード |
| 3-2 | members-stats-service.js | `ghost/core/core/server/services/stats/members-stats-service.js` | メンバー数集計ロジック |
| 3-3 | mrr-stats-service.js | `ghost/core/core/server/services/stats/mrr-stats-service.js` | MRR集計ロジック |
| 3-4 | referrers-stats-service.js | `ghost/core/core/server/services/stats/referrers-stats-service.js` | リファラー集計ロジック |
| 3-5 | posts-stats-service.js | `ghost/core/core/server/services/stats/posts-stats-service.js` | 投稿統計ロジック |

**主要処理フロー**:
- **stats-service.js 242-276行目**: `create()` - サービスの初期化とTinybird連携
- **members-stats-service.js 77-93行目**: `getCountHistory()` - メンバー数履歴の生成
- **mrr-stats-service.js 63-134行目**: `getHistory()` - MRR履歴の生成
- **referrers-stats-service.js 319-408行目**: `getTopSourcesWithRange()` - トップソース集計

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

React製のStats Appのコンポーネント構造を確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | app.tsx | `apps/stats/src/app.tsx` | アプリケーションのルート構造 |
| 4-2 | overview.tsx | `apps/stats/src/views/Stats/Overview/overview.tsx` | 統計概要画面のメインコンポーネント |
| 4-3 | use-growth-stats.ts | `apps/stats/src/hooks/use-growth-stats.ts` | 成長統計データ取得フック |

**主要処理フロー**:
- **app.tsx 9-32行目**: Providerの階層構造とルーティング設定
- **overview.tsx 66-246行目**: 概要画面のデータ取得とレンダリング
- **use-growth-stats.ts 192-393行目**: APIからのデータ取得と変換処理

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

```
Admin API Request
    │
    ├─ stats.js (API Controller)
    │      │
    │      └─ statsService.api.xxx()
    │             │
    │             └─ StatsService
    │                    │
    │                    ├─ MembersStatsService
    │                    │      ├─ getCount()
    │                    │      ├─ fetchAllStatusDeltas()
    │                    │      └─ getCountHistory()
    │                    │
    │                    ├─ MrrStatsService
    │                    │      ├─ getCurrentMrr()
    │                    │      ├─ fetchAllDeltas()
    │                    │      └─ getHistory()
    │                    │
    │                    ├─ ReferrersStatsService
    │                    │      ├─ fetchAllSignupSources()
    │                    │      ├─ fetchAllPaidConversionSources()
    │                    │      └─ getTopSourcesWithRange()
    │                    │
    │                    ├─ PostsStatsService
    │                    │      ├─ getTopPosts()
    │                    │      ├─ getPostStats()
    │                    │      └─ getNewsletterStats()
    │                    │
    │                    └─ TinybirdClient (optional)
    │                           └─ fetch()
    │
    └─ Response (JSON)
```

### データフロー図

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

APIリクエスト          StatsService                   JSONレスポンス
(date_from,     ───▶   ├─ 日付範囲計算            ───▶  {
 date_to,              ├─ DB クエリ実行                   data: [...],
 timezone,             │    ├─ members                    meta: {
 options)              │    ├─ members_status_events        totals: {...}
                       │    ├─ members_paid_subscription    }
                       │    └─ members_created_events      }
                       ├─ Tinybird クエリ（オプション）
                       └─ データ整形・正規化

Database              TinybirdService
├─ members            └─ api_kpis
├─ members_status_events   api_top_pages
├─ members_paid_subscription_events
├─ members_created_events
├─ members_subscription_created_events
├─ posts
├─ emails
├─ redirects
└─ members_click_events
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| stats.js | `ghost/core/core/server/api/endpoints/stats.js` | ソース | API エンドポイント定義 |
| index.js | `ghost/core/core/server/services/stats/index.js` | ソース | サービスエクスポート |
| service.js | `ghost/core/core/server/services/stats/service.js` | ソース | サービス初期化 |
| stats-service.js | `ghost/core/core/server/services/stats/stats-service.js` | ソース | 統計サービスファサード |
| members-stats-service.js | `ghost/core/core/server/services/stats/members-stats-service.js` | ソース | メンバー統計サービス |
| mrr-stats-service.js | `ghost/core/core/server/services/stats/mrr-stats-service.js` | ソース | MRR統計サービス |
| subscription-stats-service.js | `ghost/core/core/server/services/stats/subscription-stats-service.js` | ソース | サブスクリプション統計サービス |
| referrers-stats-service.js | `ghost/core/core/server/services/stats/referrers-stats-service.js` | ソース | リファラー統計サービス |
| posts-stats-service.js | `ghost/core/core/server/services/stats/posts-stats-service.js` | ソース | 投稿統計サービス |
| content-stats-service.js | `ghost/core/core/server/services/stats/content-stats-service.js` | ソース | コンテンツ統計サービス |
| tinybird.js | `ghost/core/core/server/services/stats/utils/tinybird.js` | ソース | Tinybird クライアント |
| date-utils.js | `ghost/core/core/server/services/stats/utils/date-utils.js` | ソース | 日付ユーティリティ |
| app.tsx | `apps/stats/src/app.tsx` | ソース | フロントエンド アプリルート |
| routes.tsx | `apps/stats/src/routes.tsx` | ソース | フロントエンド ルーティング |
| overview.tsx | `apps/stats/src/views/Stats/Overview/overview.tsx` | ソース | 統計概要画面 |
| use-growth-stats.ts | `apps/stats/src/hooks/use-growth-stats.ts` | ソース | 成長統計フック |
| global-data-provider.tsx | `apps/stats/src/providers/global-data-provider.tsx` | ソース | グローバルデータプロバイダー |
