933 lines
26 KiB
Markdown
933 lines
26 KiB
Markdown
# VRCT Python Sidecar テレメトリ(Aptabase)実装設計書
|
||
|
||
## 目次
|
||
1. [概要](#概要)
|
||
2. [基本方針](#基本方針)
|
||
3. [アーキテクチャ](#アーキテクチャ)
|
||
4. [イベント仕様](#イベント仕様)
|
||
5. [データフロー](#データフロー)
|
||
6. [API 設計](#api-設計)
|
||
7. [実装詳細](#実装詳細)
|
||
8. [テスト計画](#テスト計画)
|
||
9. [セキュリティとプライバシー](#セキュリティとプライバシー)
|
||
|
||
---
|
||
|
||
## 概要
|
||
|
||
### 目的
|
||
VRCT の **匿名な使用状況** を取得し、プロダクトの改善に役立てる。
|
||
|
||
### 特徴
|
||
- **デフォルト有効**:テレメトリはデフォルトで有効化される
|
||
- **ユーザーコントロール**:設定から任意でオン/オフ可能
|
||
- **プライバシー重視**:個人情報・入力内容・音声データは一切送信しない
|
||
- **OSS対応**:透明性と説明可能性を重視した設計
|
||
- **安定性優先**:失敗時の再送・保存は行わない
|
||
|
||
### 使用技術
|
||
- **SDK**: Aptabase Python SDK
|
||
- **ホスト**:Aptabase クラウド
|
||
- **App Key**: コード直埋め込み(環境変数・外部ファイルは未使用)
|
||
|
||
---
|
||
|
||
## 基本方針(厳守事項)
|
||
|
||
| 項目 | 方針 |
|
||
|------|------|
|
||
| **デフォルト状態** | 有効(`telemetry_enabled = true`) |
|
||
| **ユーザー制御** | 設定から任意で無効化可能 |
|
||
| **無効時の動作** | 一切の通信・スレッド・処理を停止 |
|
||
| **送信内容** | イベント名と固定属性のみ |
|
||
| **禁止データ** | 個人識別 ID、入力内容、音声データ、UI 状態 |
|
||
| **失敗時動作** | 例外を握りつぶし、再送・保存・バッファリング禁止 |
|
||
| **オフライン時** | 機能を停止するのみ(アプリ動作に影響なし) |
|
||
|
||
---
|
||
|
||
## アーキテクチャ
|
||
|
||
### ディレクトリ構成
|
||
```
|
||
src-python/
|
||
├── models/
|
||
│ └── telemetry/
|
||
│ ├── __init__.py # Public API + singleton管理
|
||
│ ├── client.py # Aptabase wrapper
|
||
│ ├── state.py # Enable/disable, last_activity
|
||
│ ├── heartbeat.py # 5分間隔heartbeatスレッド
|
||
│ └── core.py # イベント送信ロジック
|
||
└── docs/
|
||
└── details/
|
||
└── telemetry.md # 実装詳細ドキュメント
|
||
```
|
||
|
||
### コンポーネント責任分離
|
||
|
||
| モジュール | 責任 |
|
||
|-----------|------|
|
||
| `__init__.py` | パブリック API、シングルトン管理 |
|
||
| `client.py` | Aptabase SDK ラッパー、HTTP通信 |
|
||
| `state.py` | ON/OFF 状態、最終操作時刻管理 |
|
||
| `heartbeat.py` | 5分間隔スレッド、タイムアウト判定 |
|
||
| `core.py` | イベント構築・送信、重複検出 |
|
||
|
||
---
|
||
|
||
## イベント仕様
|
||
|
||
### 送信イベント一覧(固定・追加禁止)
|
||
|
||
| イベント名 | 説明 | ペイロード |
|
||
|-----------|------|----------|
|
||
| `app_started` | アプリ起動時 | `{}` |
|
||
| `app_closed` | アプリ終了時 | `{}` |
|
||
| `session_heartbeat` | 5分間隔アクティブ確認 | `{}` |
|
||
| `core_feature` | コア機能開始 | `{"feature": "translation" \| "mic_speech_to_text" \| "speaker_speech_to_text" \| "text_input"}` |
|
||
| `settings_opened` | 設定画面を開く | `{}` |
|
||
| `config_changed` | 設定変更 | `{"section": str}` |
|
||
| `error` | エラー発生 | `{"error_type": str}` |
|
||
|
||
### `core_feature` イベント詳細
|
||
|
||
機能の **実処理開始時** のみ送信(1セッション1回のみ)
|
||
|
||
#### 定義済み種別(拡張禁止)
|
||
```python
|
||
CORE_FEATURES = {
|
||
"translation": "翻訳機能を使用",
|
||
"mic_speech_to_text": "マイク音声認識(送信側)",
|
||
"speaker_speech_to_text": "スピーカー音声認識(受信側)",
|
||
"text_input": "テキスト入力(チャット送信)"
|
||
}
|
||
```
|
||
|
||
#### 送信ルール
|
||
- **マイク/スピーカーは必ず分離**:`mic_speech_to_text` と `speaker_speech_to_text` は別イベント
|
||
- **1セッション中に同一 feature は1回のみ**:重複送信禁止
|
||
- **実処理開始時のみ**:API呼び出し前の段階で送信
|
||
- **UI 状態不問**:最前面/最小化のような UI 状態は判定に使用しない
|
||
|
||
#### 実装例
|
||
```python
|
||
# 翻訳実行前
|
||
telemetry.track_core_feature("translation")
|
||
result = translator.translate(...) # 実処理
|
||
|
||
# マイク文字起こし開始時
|
||
telemetry.track_core_feature("mic_speech_to_text")
|
||
recorder.start() # 実処理
|
||
|
||
# チャット送信時
|
||
telemetry.track_core_feature("text_input")
|
||
send_message() # 実処理
|
||
```
|
||
|
||
### Session Heartbeat 仕様
|
||
|
||
#### アクティブ判定条件
|
||
以下のいずれかが発生した場合、セッションはアクティブ
|
||
|
||
1. **テキスト入力**
|
||
- チャット送信
|
||
- メッセージボックスへの入力
|
||
|
||
2. **ASR 実処理**(マイク or スピーカー)
|
||
- マイク音声認識開始
|
||
- スピーカー音声認識開始
|
||
- 実際の音声処理(UI 状態は使用しない)
|
||
|
||
#### 送信ルール
|
||
- **送信間隔**:5分(300秒)
|
||
- **タイムアウト**:最後のアクティビティから5分経過で送信停止
|
||
- **復帰条件**:アクティビティ発生 → 次の heartbeat から再開
|
||
- **無条件停止**:テレメトリ OFF 時は即座にスレッド停止
|
||
|
||
#### 実装概略図
|
||
```
|
||
Timeline:
|
||
├─ [Activity] touch_activity() →更新
|
||
├─ [300秒待機]
|
||
├─ [Check] 最後の操作 < 5分? → Yes → send heartbeat
|
||
├─ [300秒待機]
|
||
├─ [Check] 最後の操作 < 5分? → No → 待機のみ
|
||
└─ [Activity] touch_activity() →復帰 → 次heartbeat送信
|
||
```
|
||
|
||
---
|
||
|
||
## データフロー
|
||
|
||
### 初期化フロー
|
||
|
||
```
|
||
1. アプリ起動
|
||
↓
|
||
2. config.json から telemetry_enabled 読込
|
||
↓
|
||
3. telemetry.init(enabled=config.telemetry_enabled)
|
||
├─ enabled=True の場合
|
||
│ ├─ Aptabase SDK 初期化
|
||
│ ├─ heartbeat スレッド開始
|
||
│ └─ app_started イベント送信
|
||
└─ enabled=False の場合
|
||
└─ すべての処理をスキップ
|
||
```
|
||
|
||
### イベント送信フロー
|
||
|
||
```
|
||
操作発生(翻訳/ASR/テキスト入力)
|
||
↓
|
||
telemetry_enabled チェック
|
||
├─ False → 何もしない
|
||
└─ True
|
||
↓
|
||
touch_activity() → 最終時刻更新
|
||
↓
|
||
1セッション内で同一 feature 送信済み?
|
||
├─ Yes → スキップ
|
||
└─ No
|
||
↓
|
||
track_core_feature(feature_name)
|
||
↓
|
||
try:
|
||
Aptabase.track(event)
|
||
except:
|
||
握りつぶし(ログに出力しない)
|
||
```
|
||
|
||
### OFF → ON 時の復帰フロー
|
||
|
||
```
|
||
OFF 状態(設定保存)
|
||
↓
|
||
ユーザーが ON に切り替え
|
||
↓
|
||
telemetry.init(enabled=True)
|
||
├─ 既知例外は握りつぶし
|
||
├─ Aptabase 初期化
|
||
├─ heartbeat スレッド開始
|
||
└─ 次のイベントから通常送信
|
||
```
|
||
|
||
### 設定変更フロー
|
||
|
||
```
|
||
config.json 更新
|
||
↓
|
||
telemetry_enabled: false に変更
|
||
↓
|
||
telemetry.shutdown()
|
||
├─ heartbeat スレッド停止
|
||
├─ 内部状態をリセット
|
||
└─ メモリ解放
|
||
↓
|
||
OFF 中のデータは完全破棄
|
||
```
|
||
|
||
---
|
||
|
||
## API 設計
|
||
|
||
### パブリック API
|
||
|
||
```python
|
||
# =========================================================================
|
||
# INITIALIZATION & SHUTDOWN
|
||
# =========================================================================
|
||
|
||
def telemetry.init(enabled: bool) -> None:
|
||
"""
|
||
テレメトリを初期化
|
||
|
||
Args:
|
||
enabled (bool): テレメトリを有効にするか
|
||
|
||
Behavior:
|
||
- enabled=True: Aptabase SDK初期化、heartbeat開始、app_started送信
|
||
- enabled=False: すべてのスキップ、以後のtrack()は何もしない
|
||
|
||
Exception:
|
||
- SDK初期化失敗時: 例外握りつぶし、機能は停止するが無言
|
||
"""
|
||
pass
|
||
|
||
|
||
def telemetry.shutdown() -> None:
|
||
"""
|
||
テレメトリを終了
|
||
|
||
Behavior:
|
||
- app_closed イベント送信
|
||
- heartbeat スレッド停止
|
||
- 内部状態をリセット
|
||
- 例外は握りつぶし(無言)
|
||
"""
|
||
pass
|
||
|
||
|
||
# =========================================================================
|
||
# EVENT TRACKING
|
||
# =========================================================================
|
||
|
||
def telemetry.track(
|
||
event: str,
|
||
payload: dict | None = None
|
||
) -> None:
|
||
"""
|
||
汎用イベント送信
|
||
|
||
Args:
|
||
event (str): イベント名("app_started", "error" など)
|
||
payload (dict | None): イベントペイロード(デフォルト: None)
|
||
|
||
Behavior:
|
||
- enabled=False なら何もしない
|
||
- 失敗時は例外を握りつぶし(ログ出力なし)
|
||
- 再送・バッファリングなし
|
||
|
||
Example:
|
||
telemetry.track("error", {"error_type": "VRAM_ERROR"})
|
||
"""
|
||
pass
|
||
|
||
|
||
def telemetry.track_core_feature(feature: str) -> None:
|
||
"""
|
||
コア機能イベント送信(重複検出付き)
|
||
|
||
Args:
|
||
feature (str): 機能名
|
||
("translation" | "mic_speech_to_text" | "speaker_speech_to_text" | "text_input")
|
||
|
||
Behavior:
|
||
- enabled=False なら何もしない
|
||
- 1セッション内で同一 feature の重複送信を防止
|
||
- 実処理開始時に呼び出す(API呼び出し前)
|
||
- 失敗時は例外握りつぶし
|
||
|
||
Example:
|
||
telemetry.track_core_feature("translation")
|
||
result = translator.translate(...)
|
||
"""
|
||
pass
|
||
|
||
|
||
# =========================================================================
|
||
# ACTIVITY TRACKING
|
||
# =========================================================================
|
||
|
||
def telemetry.touch_activity() -> None:
|
||
"""
|
||
最終アクティビティ時刻を更新(heartbeat判定用)
|
||
|
||
Behavior:
|
||
- enabled=False なら何もしない
|
||
- 内部的に datetime.now() を記録
|
||
- 自動呼び出し: track_core_feature() 内から呼び出される
|
||
- 手動呼び出し: テキスト入力検出時など、明示的に呼び出し可
|
||
|
||
Example:
|
||
# テキスト入力時
|
||
telemetry.touch_activity()
|
||
|
||
# track_core_feature() は内部で自動呼び出し
|
||
telemetry.track_core_feature("translation")
|
||
"""
|
||
pass
|
||
|
||
|
||
# =========================================================================
|
||
# STATE QUERY
|
||
# =========================================================================
|
||
|
||
def telemetry.is_enabled() -> bool:
|
||
"""
|
||
テレメトリが有効か確認
|
||
|
||
Returns:
|
||
bool: True なら有効、False なら無効
|
||
"""
|
||
pass
|
||
|
||
|
||
def telemetry.get_state() -> dict:
|
||
"""
|
||
現在の内部状態を取得(デバッグ用)
|
||
|
||
Returns:
|
||
dict: {
|
||
"enabled": bool,
|
||
"last_activity": datetime | None,
|
||
"session_features_sent": list[str],
|
||
"heartbeat_active": bool,
|
||
}
|
||
"""
|
||
pass
|
||
```
|
||
|
||
### パブリック API 使用例
|
||
|
||
#### ケース1: アプリ起動・終了
|
||
|
||
```python
|
||
# mainloop.py
|
||
from models.telemetry import telemetry
|
||
from config import config
|
||
|
||
# 起動時
|
||
def app_startup():
|
||
telemetry.init(enabled=config.TELEMETRY_ENABLED)
|
||
# ... 他の初期化処理
|
||
|
||
# 終了時
|
||
def app_shutdown():
|
||
telemetry.shutdown()
|
||
# ... 他のクリーンアップ
|
||
```
|
||
|
||
#### ケース2: 翻訳機能
|
||
|
||
```python
|
||
# controller.py
|
||
def micMessage(self, result: dict):
|
||
# テキスト入力フェーズ(テキスト入力イベント用)
|
||
message = result["text"]
|
||
telemetry.touch_activity()
|
||
|
||
# 翻訳開始前(コア機能イベント送信)
|
||
if config.ENABLE_TRANSLATION:
|
||
telemetry.track_core_feature("translation")
|
||
translation, success = model.getInputTranslate(message)
|
||
# ... 処理続行
|
||
```
|
||
|
||
#### ケース3: マイク文字起こし
|
||
|
||
```python
|
||
# model.py
|
||
def startMicTranscript(self, fnc):
|
||
# マイク開始前
|
||
telemetry.track_core_feature("mic_speech_to_text")
|
||
|
||
mic_device = selected_mic_device[0]
|
||
self.mic_audio_recorder = SelectedMicEnergyAndAudioRecorder(...)
|
||
self.mic_audio_recorder.recordIntoQueue(...)
|
||
# ... 処理続行
|
||
```
|
||
|
||
#### ケース4: エラー報告
|
||
|
||
```python
|
||
# controller.py
|
||
except Exception as e:
|
||
is_vram_error, error_message = model.detectVRAMError(e)
|
||
if is_vram_error:
|
||
telemetry.track("error", {"error_type": "VRAM_ERROR"})
|
||
# ... エラー処理
|
||
```
|
||
|
||
#### ケース5: 設定変更
|
||
|
||
```python
|
||
# controller.py
|
||
def setUiLanguage(data):
|
||
config.UI_LANGUAGE = data
|
||
telemetry.track("config_changed", {"section": "appearance"})
|
||
return {"status": 200, "result": config.UI_LANGUAGE}
|
||
```
|
||
|
||
---
|
||
|
||
## 実装詳細
|
||
|
||
### `models/telemetry/__init__.py`
|
||
|
||
```python
|
||
"""
|
||
テレメトリ(Aptabase)管理モジュール
|
||
|
||
パブリック API を提供し、内部実装を隠蔽する。
|
||
"""
|
||
from .state import TelemetryState
|
||
from .heartbeat import HeartbeatManager
|
||
from .core import TelemetryCore
|
||
|
||
|
||
class Telemetry:
|
||
_instance = None
|
||
|
||
def __new__(cls):
|
||
if cls._instance is None:
|
||
cls._instance = super().__new__(cls)
|
||
cls._instance._initialized = False
|
||
return cls._instance
|
||
|
||
def __init__(self):
|
||
if self._initialized:
|
||
return
|
||
self.state = TelemetryState()
|
||
self.heartbeat = HeartbeatManager(self.state)
|
||
self.core = TelemetryCore(self.state)
|
||
self._initialized = True
|
||
|
||
def init(self, enabled: bool):
|
||
"""テレメトリ初期化"""
|
||
self.state.set_enabled(enabled)
|
||
if enabled:
|
||
try:
|
||
self.core.send_event("app_started")
|
||
self.heartbeat.start()
|
||
except Exception:
|
||
pass # 握りつぶし
|
||
|
||
def shutdown(self):
|
||
"""テレメトリ終了"""
|
||
if self.state.is_enabled():
|
||
try:
|
||
self.core.send_event("app_closed")
|
||
except Exception:
|
||
pass
|
||
self.heartbeat.stop()
|
||
self.state.reset()
|
||
|
||
def track(self, event: str, payload: dict = None):
|
||
"""汎用イベント送信"""
|
||
if not self.state.is_enabled():
|
||
return
|
||
try:
|
||
self.core.send_event(event, payload)
|
||
except Exception:
|
||
pass
|
||
|
||
def track_core_feature(self, feature: str):
|
||
"""コア機能イベント送信(重複検出付き)"""
|
||
if not self.state.is_enabled():
|
||
return
|
||
self.state.touch_activity()
|
||
if self.core.is_duplicate_core_feature(feature):
|
||
return
|
||
try:
|
||
self.core.send_event("core_feature", {"feature": feature})
|
||
self.state.record_feature_sent(feature)
|
||
except Exception:
|
||
pass
|
||
|
||
def touch_activity(self):
|
||
"""アクティビティ時刻更新"""
|
||
if self.state.is_enabled():
|
||
self.state.touch_activity()
|
||
|
||
def is_enabled(self) -> bool:
|
||
"""有効状態確認"""
|
||
return self.state.is_enabled()
|
||
|
||
def get_state(self) -> dict:
|
||
"""内部状態取得(デバッグ用)"""
|
||
return self.state.get_debug_info()
|
||
|
||
|
||
# Singleton instance
|
||
telemetry = Telemetry()
|
||
```
|
||
|
||
### `models/telemetry/state.py`
|
||
|
||
```python
|
||
"""
|
||
テレメトリ状態管理
|
||
- enable/disable フラグ
|
||
- 最終操作時刻
|
||
- セッション内送信済み機能リスト
|
||
"""
|
||
from datetime import datetime
|
||
from threading import Lock
|
||
|
||
|
||
class TelemetryState:
|
||
def __init__(self):
|
||
self._enabled = True # デフォルト有効
|
||
self._last_activity = None
|
||
self._session_features_sent = set()
|
||
self._lock = Lock()
|
||
|
||
def set_enabled(self, value: bool):
|
||
"""有効/無効設定"""
|
||
with self._lock:
|
||
self._enabled = bool(value)
|
||
if not self._enabled:
|
||
self._session_features_sent.clear()
|
||
|
||
def is_enabled(self) -> bool:
|
||
"""有効状態確認"""
|
||
with self._lock:
|
||
return self._enabled
|
||
|
||
def touch_activity(self):
|
||
"""最終操作時刻更新"""
|
||
with self._lock:
|
||
self._last_activity = datetime.now()
|
||
|
||
def get_last_activity(self) -> datetime:
|
||
"""最終操作時刻取得"""
|
||
with self._lock:
|
||
return self._last_activity
|
||
|
||
def record_feature_sent(self, feature: str):
|
||
"""送信済み機能を記録"""
|
||
with self._lock:
|
||
self._session_features_sent.add(feature)
|
||
|
||
def has_feature_been_sent(self, feature: str) -> bool:
|
||
"""機能がこのセッション内で送信済みか"""
|
||
with self._lock:
|
||
return feature in self._session_features_sent
|
||
|
||
def reset(self):
|
||
"""状態をリセット"""
|
||
with self._lock:
|
||
self._last_activity = None
|
||
self._session_features_sent.clear()
|
||
|
||
def get_debug_info(self) -> dict:
|
||
"""デバッグ用情報取得"""
|
||
with self._lock:
|
||
return {
|
||
"enabled": self._enabled,
|
||
"last_activity": self._last_activity.isoformat() if self._last_activity else None,
|
||
"session_features_sent": list(self._session_features_sent),
|
||
}
|
||
```
|
||
|
||
### `models/telemetry/heartbeat.py`
|
||
|
||
```python
|
||
"""
|
||
Heartbeat スレッド管理
|
||
- 5分間隔でアクティブ確認
|
||
- 操作なしで5分経過したら送信停止
|
||
"""
|
||
from datetime import datetime, timedelta
|
||
from threading import Thread, Event
|
||
import time
|
||
|
||
|
||
class HeartbeatManager:
|
||
INTERVAL = 300 # 5 minutes
|
||
TIMEOUT = 300 # 5 minutes
|
||
|
||
def __init__(self, state):
|
||
self.state = state
|
||
self.thread = None
|
||
self._stop_event = Event()
|
||
|
||
def start(self):
|
||
"""Heartbeat スレッド開始"""
|
||
if self.thread is not None and self.thread.is_alive():
|
||
return
|
||
self._stop_event.clear()
|
||
self.thread = Thread(target=self._run, daemon=True)
|
||
self.thread.start()
|
||
|
||
def stop(self):
|
||
"""Heartbeat スレッド停止"""
|
||
self._stop_event.set()
|
||
if self.thread is not None:
|
||
self.thread.join(timeout=1.0)
|
||
self.thread = None
|
||
|
||
def _run(self):
|
||
"""Heartbeat ループ"""
|
||
while not self._stop_event.is_set():
|
||
try:
|
||
time.sleep(self.INTERVAL)
|
||
|
||
if not self.state.is_enabled():
|
||
continue
|
||
|
||
last_activity = self.state.get_last_activity()
|
||
if last_activity is None:
|
||
continue
|
||
|
||
elapsed = (datetime.now() - last_activity).total_seconds()
|
||
if elapsed < self.TIMEOUT:
|
||
# アクティブなら heartbeat 送信
|
||
from .core import TelemetryCore
|
||
try:
|
||
core = TelemetryCore(self.state)
|
||
core.send_event("session_heartbeat")
|
||
except Exception:
|
||
pass # 握りつぶし
|
||
except Exception:
|
||
pass # スレッド不停止のため握りつぶし
|
||
```
|
||
|
||
### `models/telemetry/client.py`
|
||
|
||
```python
|
||
"""
|
||
Aptabase SDK ラッパー
|
||
"""
|
||
import json
|
||
from typing import Optional, Dict, Any
|
||
|
||
# Aptabase SDK のインポート
|
||
try:
|
||
from aptabase import Client as AptabaseClient
|
||
except ImportError:
|
||
AptabaseClient = None
|
||
|
||
|
||
class AptabaseWrapper:
|
||
# App Key は直埋め込み
|
||
APP_KEY = "A-SY-XXXXXXXXXXXXXXX" # 実装時に実際のキーに置き換え
|
||
|
||
def __init__(self):
|
||
self.client = None
|
||
self._init_client()
|
||
|
||
def _init_client(self):
|
||
"""Aptabase クライアント初期化"""
|
||
if AptabaseClient is None:
|
||
raise ImportError("aptabase library not installed")
|
||
try:
|
||
self.client = AptabaseClient(self.APP_KEY)
|
||
except Exception as e:
|
||
raise RuntimeError(f"Failed to initialize Aptabase: {e}")
|
||
|
||
def track(self, event_name: str, properties: Optional[Dict[str, Any]] = None):
|
||
"""イベント送信"""
|
||
if self.client is None:
|
||
raise RuntimeError("Aptabase client not initialized")
|
||
|
||
try:
|
||
# properties が None なら空辞書
|
||
if properties is None:
|
||
properties = {}
|
||
|
||
# イベント送信
|
||
self.client.track_event(event_name, properties)
|
||
except Exception as e:
|
||
# 握りつぶし:ログなし
|
||
raise
|
||
|
||
def close(self):
|
||
"""クライアントクローズ"""
|
||
if self.client is not None:
|
||
try:
|
||
# SDK のクローズ処理があれば実行
|
||
if hasattr(self.client, 'close'):
|
||
self.client.close()
|
||
except Exception:
|
||
pass
|
||
```
|
||
|
||
### `models/telemetry/core.py`
|
||
|
||
```python
|
||
"""
|
||
テレメトリコアロジック
|
||
- イベント構築・送信
|
||
- 重複検出
|
||
"""
|
||
from .client import AptabaseWrapper
|
||
from .state import TelemetryState
|
||
|
||
|
||
class TelemetryCore:
|
||
VALID_CORE_FEATURES = {
|
||
"translation",
|
||
"mic_speech_to_text",
|
||
"speaker_speech_to_text",
|
||
"text_input",
|
||
}
|
||
|
||
def __init__(self, state: TelemetryState):
|
||
self.state = state
|
||
self.client = None
|
||
try:
|
||
self.client = AptabaseWrapper()
|
||
except Exception:
|
||
# 初期化失敗時は握りつぶし
|
||
self.client = None
|
||
|
||
def send_event(self, event_name: str, payload: dict = None):
|
||
"""イベント送信"""
|
||
if self.client is None:
|
||
raise RuntimeError("Aptabase client not available")
|
||
|
||
# ペイロード準備
|
||
properties = payload or {}
|
||
|
||
# イベント送信
|
||
self.client.track(event_name, properties)
|
||
|
||
def is_duplicate_core_feature(self, feature: str) -> bool:
|
||
"""セッション内の重複チェック"""
|
||
return self.state.has_feature_been_sent(feature)
|
||
```
|
||
|
||
---
|
||
|
||
## テスト計画
|
||
|
||
### ユニットテスト
|
||
|
||
```python
|
||
# test_telemetry.py
|
||
|
||
def test_init_enabled():
|
||
"""初期化時に有効化"""
|
||
telemetry = Telemetry()
|
||
telemetry.init(enabled=True)
|
||
assert telemetry.is_enabled() is True
|
||
|
||
def test_init_disabled():
|
||
"""初期化時に無効化"""
|
||
telemetry = Telemetry()
|
||
telemetry.init(enabled=False)
|
||
assert telemetry.is_enabled() is False
|
||
|
||
def test_track_when_disabled():
|
||
"""無効時にトラック呼び出しが何もしない"""
|
||
telemetry = Telemetry()
|
||
telemetry.init(enabled=False)
|
||
telemetry.track("test_event") # 例外なし
|
||
|
||
def test_track_core_feature_no_duplicate():
|
||
"""同一feature の重複送信を防止"""
|
||
telemetry = Telemetry()
|
||
telemetry.init(enabled=True)
|
||
|
||
# 1回目送信
|
||
telemetry.track_core_feature("translation")
|
||
state = telemetry.get_state()
|
||
assert "translation" in state["session_features_sent"]
|
||
|
||
# 2回目はスキップ(内部的に重複検出)
|
||
|
||
def test_heartbeat_sends_when_active():
|
||
"""5分操作なしなら heartbeat 送信"""
|
||
# heartbeat スレッドがちゃんと5分待機後に
|
||
# touch_activity() 後のアクティブ判定で送信
|
||
|
||
def test_heartbeat_stops_when_inactive():
|
||
"""5分以上操作なしなら heartbeat 停止"""
|
||
# last_activity が 5分以上古い場合、
|
||
# heartbeat 送信しない
|
||
|
||
def test_shutdown_sends_app_closed():
|
||
"""shutdown() が app_closed を送信"""
|
||
telemetry = Telemetry()
|
||
telemetry.init(enabled=True)
|
||
telemetry.shutdown()
|
||
# app_closed が送信されたことを確認
|
||
```
|
||
|
||
### 統合テスト
|
||
|
||
```python
|
||
# test_telemetry_integration.py
|
||
|
||
def test_full_flow():
|
||
"""起動~操作~終了の全フロー"""
|
||
from models.telemetry import telemetry
|
||
|
||
# 起動
|
||
telemetry.init(enabled=True)
|
||
|
||
# 操作
|
||
telemetry.track_core_feature("translation")
|
||
telemetry.touch_activity()
|
||
|
||
# 5分待機(テスト時は短縮)
|
||
# heartbeat 送信確認
|
||
|
||
# 終了
|
||
telemetry.shutdown()
|
||
# app_closed 送信確認
|
||
```
|
||
|
||
### テスト環境での設定
|
||
|
||
```python
|
||
# config.py 修正
|
||
TELEMETRY_ENABLED = True # デフォルト有効
|
||
|
||
# config.json への保存
|
||
{
|
||
"telemetry_enabled": true
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## セキュリティとプライバシー
|
||
|
||
### データ保護方針
|
||
|
||
| 項目 | ポリシー | 実装例 |
|
||
|------|---------|-------|
|
||
| **個人識別 ID** | 送信禁止 | UUID/ユーザーIDなし |
|
||
| **入力内容** | 送信禁止 | メッセージ本文送信なし |
|
||
| **音声データ** | 送信禁止 | 音声ファイル送信なし |
|
||
| **UI 状態** | 使用禁止 | ウィンドウ座標/最小化フラグなし |
|
||
| **ログレベル** | HTTPS only | Aptabase 側で暗号化 |
|
||
|
||
### 実装チェックリスト
|
||
|
||
- [ ] イベントペイロードに個人情報が含まれていない
|
||
- [ ] マイク/スピーカーイベントが分離している
|
||
- [ ] heartbeat が UI 状態に依存していない
|
||
- [ ] OFF 時にすべての通信が停止する
|
||
- [ ] 例外が握りつぶされ、ユーザーに見えない
|
||
- [ ] Aptabase SDK の最新版を使用
|
||
- [ ] App Key がコード内に直埋め込みされている
|
||
- [ ] 環境変数・外部ファイルは使用していない
|
||
|
||
### ユーザーへの通知(OSS 表示文言)
|
||
|
||
UI に以下を表示:
|
||
|
||
> This app collects anonymous usage statistics using Aptabase.
|
||
> You can disable telemetry at any time in settings.
|
||
|
||
---
|
||
|
||
## 実装スケジュール
|
||
|
||
| フェーズ | タスク | 優先度 |
|
||
|---------|--------|--------|
|
||
| 1 | `__init__.py`, `state.py` 作成 | 高 |
|
||
| 2 | `client.py`, `core.py` 作成 | 高 |
|
||
| 3 | `heartbeat.py` 作成・テスト | 高 |
|
||
| 4 | `config.py` に `telemetry_enabled` 追加 | 高 |
|
||
| 5 | `controller.py` に API 呼び出し追加 | 高 |
|
||
| 6 | `mainloop.py` に init/shutdown 追加 | 高 |
|
||
| 7 | ユニット・統合テスト | 高 |
|
||
| 8 | ドキュメント更新 | 中 |
|
||
| 9 | UI 文言追加 | 中 |
|
||
|
||
---
|
||
|
||
## 完了条件(定義)
|
||
|
||
- [x] テレメトリ OFF 時、一切の通信が発生しない
|
||
- [x] オフライン状態でもアプリが正常に動作する
|
||
- [x] `core_feature` は1セッション中に1回のみ送信される
|
||
- [x] heartbeat は 5分以上操作がないと送信を停止する
|
||
- [x] 個人情報・入力内容・音声データは絶対に送信されない
|
||
- [x] 送信失敗時は例外を握りつぶし、無言で続行する
|
||
- [x] マイク/スピーカーイベントが分離している
|
||
|
||
---
|
||
|
||
## 参考資料
|
||
|
||
- [Aptabase Python SDK](https://github.com/aptabase/aptabase-python)
|
||
- [Aptabase Dashboard](https://aptabase.com)
|
||
- [Privacy by Design](https://en.wikipedia.org/wiki/Privacy_by_design)
|
||
|