# 機能設計書 13-メンバーアクティビティ

## 概要

本ドキュメントは、Ghostにおけるメンバーアクティビティ追跡機能の設計仕様を記載する。本機能は、メンバーのログイン・閲覧・購読などの活動履歴を追跡し、管理画面での分析やエンゲージメント向上のための情報を提供する。

### 本機能の処理概要

**業務上の目的・背景**：
サイト運営者がメンバーのエンゲージメントを理解し、コンテンツ戦略を改善するためには、メンバーがいつ・どのようにサイトを利用しているかを把握する必要がある。最終閲覧日時、コメント投稿日時、メール開封、リンククリックなどのアクティビティを追跡することで、アクティブなメンバーの識別や休眠メンバーへのアプローチが可能となる。

**機能の利用シーン**：
- メンバーの最終訪問日を確認する場合
- エンゲージメントの高いメンバーを特定する場合
- 休眠メンバーに対するリエンゲージメント施策を計画する場合
- 個別メンバーの活動履歴を確認する場合

**主要な処理内容**：
1. ページ閲覧イベントの検知と last_seen_at 更新
2. コメント投稿イベントの検知と last_commented_at 更新
3. メール開封イベントの検知と last_seen_at 更新
4. リンククリックイベントの検知と last_seen_at 更新
5. メンバー作成・サブスクリプション作成イベントの保存

**関連システム・外部連携**：
- DomainEventsサービス（イベント駆動アーキテクチャ）
- メール分析サービス（開封イベント連携）
- リンクトラッキングサービス（クリックイベント連携）

**権限による制御**：
- メンバー閲覧権限でアクティビティ情報を参照可能

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 18 | メンバー詳細画面 | 主画面 | メンバーの活動履歴表示 |
| 20 | メンバーアクティビティ画面 | 主画面 | 全メンバーの活動ログ一覧表示 |

## 機能種別

イベント処理 / データ追跡

## 入力仕様

### 入力パラメータ（イベント）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| memberId | string | Yes | メンバーID | 存在チェック |
| memberLastSeenAt | Date | No | 既知の最終閲覧日時 | キャッシュ比較用 |
| timestamp | Date | Yes | イベント発生日時 | - |

### 入力データソース

- MemberPageViewEvent（ページ閲覧）
- MemberCommentEvent（コメント投稿）
- MemberLinkClickEvent（リンククリック）
- EmailOpenedEvent（メール開封）
- MemberCreatedEvent（メンバー作成）
- SubscriptionCreatedEvent（サブスクリプション作成）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| last_seen_at | datetime | 最終閲覧日時 |
| last_commented_at | datetime | 最終コメント日時 |
| member_created_events | Array | メンバー作成イベント履歴 |
| subscription_created_events | Array | サブスクリプション作成イベント履歴 |

### 出力先

- membersテーブル（last_seen_at, last_commented_at）
- members_created_eventsテーブル（属性情報付き）
- members_subscription_created_eventsテーブル（属性情報付き）

## 処理フロー

### 処理シーケンス

```
1. DomainEventの受信
   └─ 各種イベントをサブスクライブ
2. キャッシュチェック（last_seen_at更新時）
   └─ 同一日の更新をスキップ
3. データベース更新
   └─ トランザクション内でロック取得後更新
4. 追加イベント発火
   └─ member.edited イベントの発火
```

### フローチャート

```mermaid
flowchart TD
    A[イベント受信] --> B{イベント種別}
    B -->|PageView/LinkClick| C[キャッシュチェック]
    B -->|Comment| D[last_seen_at + last_commented_at 更新]
    B -->|EmailOpened| E[last_seen_at 更新（キャッシュなし）]
    B -->|MemberCreated| F[members_created_events INSERT]
    B -->|SubscriptionCreated| G[subscription_created_events INSERT]
    C -->|キャッシュヒット| H[スキップ]
    C -->|キャッシュミス| I[last_seen_at 更新]
    I --> J[member.edited イベント発火]
    D --> J
    E --> J
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-13-01 | 日次更新 | last_seen_at は同一日内では1回のみ更新 | ページ閲覧/クリック時 |
| BR-13-02 | タイムゾーン考慮 | サイト設定のタイムゾーンで日付判定 | last_seen_at 更新時 |
| BR-13-03 | コメント時は両方更新 | コメント投稿時は last_seen_at と last_commented_at の両方を更新 | コメント投稿時 |
| BR-13-04 | ロック取得 | 更新時は行ロックを取得して競合防止 | DB更新時 |
| BR-13-05 | キャッシュ活用 | メモリキャッシュで不要な更新を抑制 | ページ閲覧時 |
| BR-13-06 | 属性情報保存 | イベント保存時にUTMパラメータ等の属性情報も記録 | イベント保存時 |

### 計算ロジック

- 日付判定: `moment(timestamp).tz(timezone).startOf('day')` でサイトタイムゾーンでの日付開始を判定

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 最終閲覧更新 | members | UPDATE | last_seen_at 更新 |
| 最終コメント更新 | members | UPDATE | last_commented_at 更新 |
| メンバー作成イベント | members_created_events | INSERT | 属性情報付きで記録 |
| サブスク作成イベント | members_subscription_created_events | INSERT | 属性情報付きで記録 |

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

#### members

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | last_seen_at | イベントのタイムスタンプ | 日次で1回のみ |
| UPDATE | last_commented_at | イベントのタイムスタンプ | コメント時のみ |

#### members_created_events

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | member_id | イベントのmemberId | - |
| INSERT | created_at | イベントのtimestamp | - |
| INSERT | attribution_id | 属性ID | nullable |
| INSERT | attribution_url | 属性URL | nullable |
| INSERT | attribution_type | 属性タイプ | nullable |
| INSERT | source | ソース | - |
| INSERT | referrer_source | リファラーソース | nullable |
| INSERT | referrer_medium | リファラーメディア | nullable |
| INSERT | referrer_url | リファラーURL | nullable |
| INSERT | utm_source | UTMソース | nullable |
| INSERT | utm_medium | UTMメディア | nullable |
| INSERT | utm_campaign | UTMキャンペーン | nullable |
| INSERT | utm_term | UTMターム | nullable |
| INSERT | utm_content | UTMコンテンツ | nullable |
| INSERT | batch_id | バッチID | nullable |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | ログ出力 | メンバーが見つからない | エラーログ出力し継続 |
| - | ログ出力 | DB更新失敗 | エラーログ出力、キャッシュから削除 |

### リトライ仕様

- キャッシュ更新失敗時はキャッシュをクリアして次回イベントで再試行

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

- last_seen_at更新: FOR UPDATE ロックを取得後に更新
- イベント保存: 単一INSERT（トランザクション不要）

## パフォーマンス要件

- キャッシュ活用: 同一日の重複更新を防止
- 非同期処理: イベント駆動で非同期実行

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

- 内部イベントのみ処理（外部からのイベント注入不可）
- メンバーIDの存在検証

## 備考

- clickTrackingLastSeenAtUpdater: 設定でリンククリックによる更新を無効化可能

---

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

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

### 推奨読解順序

#### Step 1: イベントサービスの構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | index.js | `ghost/core/core/server/services/members-events/index.js` | サービス初期化、依存関係のワイヤリング |

**読解のコツ**:
- `MembersEventsServiceWrapper` クラスで各コンポーネントを初期化
- EventStorage と LastSeenAtUpdater の2つの主要コンポーネントを理解

**主要処理フロー**:
- **16-28行目**: EventStorage初期化（モデル注入）
- **39-51行目**: LastSeenAtUpdater初期化（キャッシュ、設定注入）
- **54-55行目**: DomainEventsへのサブスクライブ

#### Step 2: イベント保存処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | event-storage.js | `ghost/core/core/server/services/members-events/event-storage.js` | イベントのDB保存ロジック |

**主要処理フロー**:
- **25-44行目**: MemberCreatedEventのサブスクライブと保存
- **47-67行目**: SubscriptionCreatedEventのサブスクライブと保存
- 属性情報（UTMパラメータ等）の保存を確認

#### Step 3: 最終閲覧日更新を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | last-seen-at-updater.js | `ghost/core/core/server/services/members-events/last-seen-at-updater.js` | last_seen_at更新ロジック |

**主要処理フロー**:
- **48-88行目**: subscribe() - 各イベントタイプへのサブスクライブ
- **98-113行目**: updateLastSeenAtWithoutKnownLastSeen() - メール開封用更新
- **118-122行目**: cachedUpdateLastSeenAt() - キャッシュ経由の更新
- **132-163行目**: updateLastSeenAt() - メイン更新ロジック（ロック取得）
- **173-189行目**: updateLastCommentedAt() - コメント時の更新

#### Step 4: キャッシュ機構を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | last-seen-at-cache.js | `ghost/core/core/server/services/members-events/last-seen-at-cache.js` | メモリキャッシュ実装 |

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

```
DomainEvents
    │
    ├─ MemberCreatedEvent
    │      └─ EventStorage.subscribe()
    │             └─ MemberCreatedEvent.add()
    │
    ├─ SubscriptionCreatedEvent
    │      └─ EventStorage.subscribe()
    │             └─ SubscriptionCreatedEvent.add()
    │
    ├─ MemberPageViewEvent
    │      └─ LastSeenAtUpdater.subscribe()
    │             └─ cachedUpdateLastSeenAt()
    │                    ├─ lastSeenAtCache.shouldUpdateMember()
    │                    └─ updateLastSeenAt()
    │                           └─ members.save() [with lock]
    │
    ├─ MemberLinkClickEvent
    │      └─ LastSeenAtUpdater.subscribe() [configurable]
    │             └─ cachedUpdateLastSeenAt()
    │
    ├─ MemberCommentEvent
    │      └─ LastSeenAtUpdater.subscribe()
    │             └─ updateLastCommentedAt()
    │
    └─ EmailOpenedEvent
           └─ LastSeenAtUpdater.subscribe()
                  └─ updateLastSeenAtWithoutKnownLastSeen()
```

### データフロー図

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

DomainEvents          EventStorage /
(各種イベント)         LastSeenAtUpdater
   │                         │
   │                         │
   ├─ MemberCreated ────────▶├─────▶ members_created_events
   │                         │
   ├─ SubscriptionCreated ──▶├─────▶ subscription_created_events
   │                         │
   ├─ PageView ─────────────▶├─────▶ members.last_seen_at
   │     (キャッシュ経由)       │
   ├─ LinkClick ────────────▶├─────▶ members.last_seen_at
   │                         │
   ├─ Comment ──────────────▶├─────▶ members.last_seen_at
   │                         │       members.last_commented_at
   │                         │
   └─ EmailOpened ──────────▶└─────▶ members.last_seen_at
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.js | `ghost/core/core/server/services/members-events/index.js` | ソース | サービスラッパー |
| event-storage.js | `ghost/core/core/server/services/members-events/event-storage.js` | ソース | イベント保存 |
| last-seen-at-updater.js | `ghost/core/core/server/services/members-events/last-seen-at-updater.js` | ソース | 最終閲覧更新 |
| last-seen-at-cache.js | `ghost/core/core/server/services/members-events/last-seen-at-cache.js` | ソース | キャッシュ |
| event-repository.js | `ghost/core/core/server/services/members/members-api/repositories/event-repository.js` | ソース | イベントリポジトリ |
| members.js | `ghost/core/core/server/api/endpoints/members.js` | ソース | activityFeed API |
