# 機能設計書 92-メンバー属性

## 概要

本ドキュメントは、メンバー獲得経路・リファラーなどを追跡するメンバー属性（Member Attribution）機能について、その設計・仕様を詳細に記載したものである。

### 本機能の処理概要

メンバー属性機能は、サイト訪問者がメンバー登録またはサブスクリプション購入に至るまでの経路（アトリビューション）を追跡・分析する機能である。これにより、マーケティング効果の測定やコンテンツ戦略の最適化が可能になる。

**業務上の目的・背景**：デジタルマーケティングにおいて、メンバー獲得に貢献したコンテンツやチャネルを特定することは重要である。本機能により、どの記事・タグ・著者ページがメンバー登録につながったか、どのリファラー（Google検索、Twitter、ニュースレター等）から流入したかを追跡できる。これによりコンテンツ制作やマーケティング施策のROI測定が可能になる。

**機能の利用シーン**：
- 訪問者がサイト内を回遊し、最終的にメンバー登録する際の経路追跡
- 有料サブスクリプション購入時のアトリビューション記録
- 管理画面でメンバーの獲得経路を確認
- 記事・コンテンツごとのコンバージョン分析
- UTMパラメータを使用したキャンペーン効果測定

**主要な処理内容**：
1. フロントエンドでの訪問履歴（URLHistory）の記録
2. リファラー情報・UTMパラメータの解析
3. 「Last Post Algorithm」によるアトリビューション決定
4. メンバー登録・サブスクリプション作成イベントへのアトリビューション紐付け
5. アウトバウンドリンクへのref パラメータ付与

**関連システム・外部連携**：
- フロントエンドスクリプト（member-attribution.js）がブラウザのsessionStorageを使用
- @tryghost/referrer-parserパッケージを使用してリファラー解析
- Stripe決済との連携（サブスクリプション作成時のアトリビューション）

**権限による制御**：アトリビューション追跡はmembers_track_sources設定により有効/無効を制御可能。アウトバウンドリンクタギングはoutbound_link_tagging設定で制御。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 24 | 成長分析画面 | 補助機能 | 獲得経路・リファラー分析 |
| 29 | 投稿成長分析画面 | 補助機能 | 投稿経由の獲得経路分析 |

## 機能種別

データ追跡 / 分析 / イベント記録

## 入力仕様

### 入力パラメータ

#### フロントエンドURL履歴

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| path | string | No | 訪問パス（例: /welcome/） | 有効なパス形式 |
| id | string | No | リソースID（手動追加時） | 24文字以下 |
| type | string | No | リソースタイプ | 'post'のみ許可 |
| referrerSource | string | No | リファラーソース | - |
| referrerMedium | string | No | リファラーメディア | - |
| referrerUrl | string | No | リファラーURL | 有効なURL形式 |
| utmSource | string | No | UTMソース | - |
| utmMedium | string | No | UTMメディウム | - |
| utmCampaign | string | No | UTMキャンペーン | - |
| utmTerm | string | No | UTMキーワード | - |
| utmContent | string | No | UTMコンテンツ | - |
| time | number | Yes | タイムスタンプ | 整数値（24時間以内） |

#### URL パラメータ（クエリストリング）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| attribution_id | string | No | アトリビューションID | - |
| attribution_type | string | No | アトリビューションタイプ | - |
| ref | string | No | リファラー（Ghost newsletter等） | - |
| source | string | No | ソース | - |
| utm_source | string | No | UTMソース | - |
| utm_medium | string | No | UTMメディウム | - |
| utm_campaign | string | No | UTMキャンペーン | - |
| utm_term | string | No | UTMキーワード | - |
| utm_content | string | No | UTMコンテンツ | - |

### 入力データソース

- ブラウザsessionStorage: ghost-historyキー
- document.referrer: ブラウザリファラー
- window.location: 現在のURL
- URLSearchParams: クエリパラメータ

## 出力仕様

### 出力データ

#### Attribution オブジェクト

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string/null | アトリビューションリソースID |
| url | string/null | アトリビューションURL（絶対URL） |
| type | string/null | タイプ（page/post/author/tag/url） |
| title | string/null | リソースタイトル |
| referrerSource | string/null | リファラーソース |
| referrerMedium | string/null | リファラーメディウム |
| referrerUrl | string/null | リファラーURL |
| utmSource | string/null | UTMソース |
| utmMedium | string/null | UTMメディウム |
| utmCampaign | string/null | UTMキャンペーン |
| utmTerm | string/null | UTMキーワード |
| utmContent | string/null | UTMコンテンツ |

### 出力先

- members_created_events テーブル: メンバー作成時のアトリビューション
- members_subscription_created_events テーブル: サブスクリプション作成時のアトリビューション
- donation_payment_events テーブル: 寄付時のアトリビューション

## 処理フロー

### 処理シーケンス

```
1. ページ読み込み時（フロントエンド）
   └─ member-attribution.js が実行される
2. 履歴読み込み
   └─ sessionStorageからghost-historyを取得
3. 期限切れ履歴の削除
   └─ 24時間以上前のエントリを削除
4. リファラー解析
   └─ parseReferrerData()でUTMパラメータ等を抽出
5. 履歴更新
   └─ 現在のパスと属性情報を追加
6. 履歴保存
   └─ sessionStorageに保存（最大15件）
7. メンバー登録時（バックエンド）
   └─ MemberAttributionService.getAttribution()を呼び出し
8. アトリビューション決定
   └─ Last Post Algorithmでアトリビューションを決定
9. イベント記録
   └─ members_created_eventsにアトリビューションを保存
```

### フローチャート

```mermaid
flowchart TD
    subgraph Frontend
        A[ページ読み込み] --> B[sessionStorage読み込み]
        B --> C{履歴あり?}
        C -->|Yes| D[期限切れ削除]
        C -->|No| E[空配列作成]
        D --> F[リファラー解析]
        E --> F
        F --> G{attribution_id あり?}
        G -->|Yes| H[属性付き履歴追加]
        G -->|No| I[パス履歴追加]
        H --> J[履歴制限チェック]
        I --> J
        J --> K[sessionStorage保存]
    end

    subgraph Backend
        L[メンバー登録リクエスト] --> M{追跡有効?}
        M -->|No| N[null返却]
        M -->|Yes| O[UrlHistory作成]
        O --> P{履歴あり?}
        P -->|No| N
        P -->|Yes| Q[リファラー解析]
        Q --> R[Last Post Algorithm]
        R --> S{Post見つかった?}
        S -->|Yes| T[Postアトリビューション]
        S -->|No| U{Page/Tag/Author見つかった?}
        U -->|Yes| V[リソースアトリビューション]
        U -->|No| W[URLアトリビューション]
        T --> X[Attribution返却]
        V --> X
        W --> X
    end

    K -.->|メンバー登録時| L
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-01 | Last Post Algorithm | 履歴内で最新の投稿（post）を優先的にアトリビューションとする | アトリビューション決定時 |
| BR-02 | 24時間有効期限 | 24時間以上前の履歴エントリは無効 | 履歴処理時 |
| BR-03 | 履歴上限 | 履歴は最大15件まで保持 | フロントエンド |
| BR-04 | 追跡有効化 | members_track_sources設定がtrueの場合のみ追跡 | サーバーサイド |
| BR-05 | 優先順位 | Post > Page/Tag/Author > URL の順で優先 | アトリビューション決定時 |
| BR-06 | リファラー解析 | ref > source > utm_source の順でソース決定 | リファラー解析時 |
| BR-07 | 同一ドメイン除外 | 同一ドメインからのリファラーは除外 | リファラー解析時 |
| BR-08 | Stripe除外 | checkout.stripe.comからのリファラーは無視 | リファラー解析時 |
| BR-09 | コンテキスト判定 | import/api/adminコンテキストでは特別なアトリビューション設定 | サーバーサイド |
| BR-10 | アウトバウンドタギング | outbound_link_tagging設定がtrueの場合に外部リンクへref追加 | メール送信時 |

### 計算ロジック

**Last Post Algorithm**：
1. 履歴を新しい順にイテレート
2. 最初に見つかった「post」タイプのリソースをアトリビューションとして返却
3. postが見つからない場合、最初のID付きリソース（page/tag/author）を返却
4. ID付きリソースがない場合、最初のURL履歴を返却
5. すべて失敗した場合、リファラー情報のみのアトリビューションを返却

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| メンバー作成 | members_created_events | INSERT | アトリビューション情報を記録 |
| サブスクリプション作成 | members_subscription_created_events | INSERT | アトリビューション情報を記録 |
| 寄付 | donation_payment_events | INSERT | アトリビューション情報を記録 |
| アトリビューション取得 | members_created_events | SELECT | メンバーのアトリビューション取得 |
| アトリビューション取得 | members_subscription_created_events | SELECT | サブスクリプションのアトリビューション取得 |
| リソース取得 | posts | SELECT | アトリビューションリソース情報取得 |
| リソース取得 | users | SELECT | 著者情報取得 |
| リソース取得 | tags | SELECT | タグ情報取得 |

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

#### members_created_events

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | attribution_id | リソースID | 最大24文字 |
| INSERT | attribution_type | url/post/page/author/tag | 列挙型 |
| INSERT | attribution_url | アトリビューションURL | 最大2000文字 |
| INSERT | referrer_source | リファラーソース | 最大191文字 |
| INSERT | referrer_medium | リファラーメディウム | 最大191文字 |
| INSERT | referrer_url | リファラーURL | 最大2000文字 |
| INSERT | utm_source | UTMソース | 最大191文字 |
| INSERT | utm_medium | UTMメディウム | 最大191文字 |
| INSERT | utm_campaign | UTMキャンペーン | 最大191文字 |
| INSERT | utm_term | UTMキーワード | 最大191文字 |
| INSERT | utm_content | UTMコンテンツ | 最大191文字 |
| SELECT | attribution_id, attribution_type, etc. | member_id = {対象メンバーID} | リレーション付き |

#### members_subscription_created_events

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | attribution_id | リソースID | 最大24文字 |
| INSERT | attribution_type | url/post/page/author/tag | 列挙型 |
| INSERT | attribution_url | アトリビューションURL | 最大2000文字 |
| INSERT | referrer_source | リファラーソース | 最大191文字 |
| INSERT | referrer_medium | リファラーメディウム | 最大191文字 |
| INSERT | referrer_url | リファラーURL | 最大2000文字 |
| INSERT | utm_source | UTMソース | 最大191文字 |
| INSERT | utm_medium | UTMメディウム | 最大191文字 |
| INSERT | utm_campaign | UTMキャンペーン | 最大191文字 |
| INSERT | utm_term | UTMキーワード | 最大191文字 |
| INSERT | utm_content | UTMコンテンツ | 最大191文字 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | 警告 | sessionStorage解析失敗 | console.warnログ出力、空履歴で継続 |
| - | エラー | リファラー解析失敗 | console.errorログ出力、nullで継続 |
| - | 警告 | URLパラメータ解析失敗 | console.errorログ出力、処理継続 |
| - | 無視 | Integration取得失敗 | エラー無視で継続 |

### リトライ仕様

リトライ処理なし。フロントエンドでのエラーは警告ログを出力して処理継続。

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

アトリビューション記録はメンバー作成/サブスクリプション作成トランザクションの一部として実行される。

## パフォーマンス要件

- フロントエンドスクリプトはページ読み込み時に非同期で実行
- sessionStorage操作はブロッキングだが高速
- 履歴上限（15件）により無限増大を防止
- 24時間有効期限によりセッション跨ぎでの累積を防止

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

- sessionStorageはサイトオリジン内でのみアクセス可能
- セッション終了時に自動削除（ブラウザ依存）
- UTMパラメータは最大191文字に制限
- referrer_urlは最大2000文字に制限
- members_track_sources設定により追跡を無効化可能
- outbound_link_tagging設定によりリンクタギングを無効化可能

## 備考

- フロントエンドスクリプトはブラウザのsessionStorageを使用（localStorage ではない）
- Portal内のハッシュURL（#/portal）も適切にパラメータ抽出可能
- Facebookドメイン等、一部ドメインへのref付与はブロック（制限あり）

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | url-history.js | `ghost/core/core/server/services/member-attribution/url-history.js` | URLHistoryItem型定義と有効期限ロジック |
| 1-2 | attribution-builder.js | `ghost/core/core/server/services/member-attribution/attribution-builder.js` | AttributionResource型定義 |
| 1-3 | schema.js | `ghost/core/core/server/data/schema/schema.js` | members_created_events, members_subscription_created_eventsのスキーマ |

**読解のコツ**:
- url-history.jsの1-19行目でUrlHistoryItem型を確認
- attribution-builder.jsの1-15行目でAttributionResource型を確認
- ALLOWED_TYPES配列で許可されるタイプを確認（'post'のみ）

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | member-attribution.js | `ghost/core/core/frontend/src/member-attribution/member-attribution.js` | ブラウザでの履歴記録ロジック |
| 2-2 | url-attribution.js | `ghost/core/core/frontend/src/utils/url-attribution.js` | URLパラメータ解析ユーティリティ |

**主要処理フロー**:
1. **36-52行目**: sessionStorageからの履歴読み込み
2. **54-80行目**: 期限切れエントリの削除
3. **82-101行目**: parseReferrerData()でリファラー解析
4. **118-140行目**: attribution_id/typeパラメータ処理
5. **144-165行目**: 履歴エントリの追加・更新
6. **167-173行目**: 履歴制限と保存

#### Step 3: サービス初期化を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | index.js | `ghost/core/core/server/services/member-attribution/index.js` | サービスの依存関係注入 |

**主要処理フロー**:
- **7-12行目**: init()でのサービス初期化チェック
- **21-29行目**: UrlTranslatorの初期化
- **31-34行目**: ReferrerTranslatorの初期化
- **36行目**: AttributionBuilderの初期化
- **38-42行目**: OutboundLinkTaggerの初期化
- **45-53行目**: MemberAttributionServiceの初期化

#### Step 4: メインサービスを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | member-attribution-service.js | `ghost/core/core/server/services/member-attribution/member-attribution-service.js` | アトリビューション取得・記録のメインロジック |

**主要処理フロー**:
- **28-69行目**: getAttributionFromContext()でコンテキストベースのアトリビューション
- **76-82行目**: getAttribution()で履歴からのアトリビューション取得
- **91-104行目**: addPostAttributionTracking()でURLへの属性追加
- **112-137行目**: getEventAttribution()でイベントモデルからの取得
- **144-158行目**: getMemberCreatedAttribution()でメンバー作成時のアトリビューション取得
- **200-216行目**: _resolveContextSource()でコンテキストソース判定

#### Step 5: アトリビューション決定ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | attribution-builder.js | `ghost/core/core/server/services/member-attribution/attribution-builder.js` | Last Post Algorithmの実装 |

**主要処理フロー**:
- **172-187行目**: getAttribution()のエントリポイント
- **189-198行目**: リファラーデータの取得
- **204-218行目**: 最初のpost検索ループ
- **222-229行目**: id付きリソースのフォールバック
- **233-238行目**: URL履歴へのフォールバック

#### Step 6: URL・リファラー翻訳を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 6-1 | url-translator.js | `ghost/core/core/server/services/member-attribution/url-translator.js` | URLからリソースへの変換 |
| 6-2 | referrer-translator.js | `ghost/core/core/server/services/member-attribution/referrer-translator.js` | リファラー情報の解析 |

**主要処理フロー（url-translator.js）**:
- **60-86行目**: getResourceDetails()でURLHistoryItemからリソース取得
- **92-125行目**: getTypeAndIdFromPath()でパスからタイプ・ID取得
- **131-159行目**: getResourceById()でDBからリソース取得

**主要処理フロー（referrer-translator.js）**:
- **38-103行目**: getReferrerDetails()でリファラー解析
- **65-75行目**: UTMデータの抽出（最古のエントリから）
- **78-95行目**: リファラーソースの解析（最新のエントリから）

#### Step 7: アウトバウンドリンクタギングを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 7-1 | outbound-link-tagger.js | `ghost/core/core/server/services/member-attribution/outbound-link-tagger.js` | 外部リンクへのref付与 |

**主要処理フロー**:
- **4-14行目**: blockedReferrerDomainsの定義
- **48-84行目**: addToUrl()でURLへのref追加
- **86-97行目**: addToHtml()でHTML内リンクへの一括追加

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

```
[フロントエンド - ページ読み込み]
    │
    └─ member-attribution.js
           │
           ├─ sessionStorage.getItem('ghost-history')
           │
           ├─ parseReferrerData()
           │      └─ url-attribution.js::extractParams()
           │
           ├─ getReferrer()
           │      └─ url-attribution.js::selectPrimaryReferrer()
           │
           └─ sessionStorage.setItem('ghost-history')


[バックエンド - メンバー登録時]
    │
    └─ MemberAttributionService.getAttribution(history)
           │
           ├─ UrlHistory.create(historyArray)
           │      └─ isValidHistory() & MAX_AGE フィルタリング
           │
           └─ AttributionBuilder.getAttribution(history)
                  │
                  ├─ ReferrerTranslator.getReferrerDetails(history)
                  │      └─ ReferrerParser.parse()
                  │
                  └─ UrlTranslator.getResourceDetails(item)
                         │
                         ├─ urlService.getResource(path)
                         │
                         └─ getResourceById(id, type)
                                ├─ Post.findOne()
                                ├─ User.findOne()
                                └─ Tag.findOne()


[バックエンド - コンテキストからのアトリビューション]
    │
    └─ MemberAttributionService.getAttributionFromContext(context)
           │
           ├─ _resolveContextSource(context)
           │
           └─ Integration.findOne() [integration context時]
```

### データフロー図

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

ブラウザ訪問 ─────────┐
  - path              │
  - referrer          │
  - UTMパラメータ    │
                      ▼
                member-attribution.js ──────► sessionStorage
                      │                         (ghost-history)
                      │
                      │
メンバー登録 ─────────┤
  - history[]        │
                     ▼
              MemberAttributionService
                     │
                     ├─► UrlHistory.create()
                     │
                     ├─► AttributionBuilder.getAttribution()
                     │        │
                     │        ├─► ReferrerTranslator
                     │        │
                     │        └─► UrlTranslator
                     │
                     ▼
              Attribution オブジェクト ──────► members_created_events
                                               members_subscription_created_events
                                               donation_payment_events


[アウトバウンドリンクタギング]

HTML コンテンツ ─────► OutboundLinkTagger.addToHtml() ─────► 変換済みHTML
                              │                               (ref パラメータ付き)
                              ├─► LinkReplacer.replace()
                              │
                              └─► addToUrl()
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| member-attribution.js | `ghost/core/core/frontend/src/member-attribution/member-attribution.js` | ソース | フロントエンド履歴記録 |
| url-attribution.js | `ghost/core/core/frontend/src/utils/url-attribution.js` | ソース | URLパラメータ解析ユーティリティ |
| index.js | `ghost/core/core/server/services/member-attribution/index.js` | ソース | サービス初期化 |
| member-attribution-service.js | `ghost/core/core/server/services/member-attribution/member-attribution-service.js` | ソース | メインサービス |
| attribution-builder.js | `ghost/core/core/server/services/member-attribution/attribution-builder.js` | ソース | アトリビューション構築・決定ロジック |
| url-history.js | `ghost/core/core/server/services/member-attribution/url-history.js` | ソース | URL履歴管理・バリデーション |
| url-translator.js | `ghost/core/core/server/services/member-attribution/url-translator.js` | ソース | URL・リソース変換 |
| referrer-translator.js | `ghost/core/core/server/services/member-attribution/referrer-translator.js` | ソース | リファラー解析 |
| outbound-link-tagger.js | `ghost/core/core/server/services/member-attribution/outbound-link-tagger.js` | ソース | アウトバウンドリンクタギング |
| member-created-event.js | `ghost/core/core/server/models/member-created-event.js` | モデル | メンバー作成イベントモデル |
| subscription-created-event.js | `ghost/core/core/server/models/subscription-created-event.js` | モデル | サブスクリプション作成イベントモデル |
| schema.js | `ghost/core/core/server/data/schema/schema.js` | スキーマ | データベーススキーマ定義 |
| attribution.test.js | `ghost/core/test/unit/server/services/member-attribution/attribution.test.js` | テスト | アトリビューション構築テスト |
| service.test.js | `ghost/core/test/unit/server/services/member-attribution/service.test.js` | テスト | サービステスト |
| history.test.js | `ghost/core/test/unit/server/services/member-attribution/history.test.js` | テスト | 履歴テスト |
| referrer-translator.test.js | `ghost/core/test/unit/server/services/member-attribution/referrer-translator.test.js` | テスト | リファラー解析テスト |
| url-translator.test.js | `ghost/core/test/unit/server/services/member-attribution/url-translator.test.js` | テスト | URL変換テスト |
| outbound-link-tagger.test.js | `ghost/core/test/unit/server/services/member-attribution/outbound-link-tagger.test.js` | テスト | リンクタギングテスト |
