はじめに
iOSアプリの運用を始めると、すぐに以下のような課題に直面します。
- クラッシュしているのか、どこで落ちているのか知りたい
- ダウンロード数やセッション数を把握したい
- 古いバージョンのユーザーにアップデートを促したい
- レビューを書いてもらいたい
多くの記事が Firebase Crashlytics + Firebase Analytics の導入を勧めますが、これはGoogleのサーバーにユーザーのデバイス情報・使用状況・クラッシュログなどを送信することを意味します。App Storeへの提出時にも「データ収集・追跡」の申告が必要になります。
個人開発や学術目的のアプリでは、そこまでのデータ収集が本当に必要でしょうか?
本記事では、ユーザーデータを外部に送信せず、Apple標準ツールとOSSだけで運用する構成を、実際のApp Store公開アプリ KotenOCR での経験をもとに紹介します。
構成の全体像
| 目的 | Firebase構成 | 本記事の構成 | 外部送信 |
|---|---|---|---|
| クラッシュ計測 | Crashlytics | MetricKit | なし |
| アナリティクス | Firebase Analytics | App Store Connect Analytics API | なし |
| アップデート促進 | Remote Config | Siren(iTunes Lookup API) | なし※ |
| レビュー促進 | In-App Messaging | SKStoreReviewRequest | なし |
| クラッシュログ閲覧 | Crashlytics Console | Xcode Organizer | なし |
※ Sirenは iTunes Lookup API(Apple公式)にバージョン確認のリクエストを送りますが、ユーザーの個人情報は含まれません。
1. MetricKit — クラッシュ計測
MetricKit はiOS 13+で利用可能なApple標準フレームワークです。クラッシュレポート、ハング(応答なし)、起動時間などのパフォーマンスデータをアプリ内で受け取れます。
実装
import MetricKit
class MetricKitManager: NSObject, MXMetricManagerSubscriber {
static let shared = MetricKitManager()
func start() {
MXMetricManager.shared.add(self)
}
// iOS 15+: クラッシュ・ハングの即時通知
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
if let crashDiagnostics = payload.crashDiagnostics {
for crash in crashDiagnostics {
// クラッシュのスタックトレースを処理
let description = crash.callStackTree.jsonRepresentation()
// ログに記録、またはローカルに保存
}
}
}
}
// iOS 14+: 24時間ごとのパフォーマンスメトリクス
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
// 起動時間、メモリ使用量、ディスクI/Oなど
}
}
}
```text
アプリの起動時に `MetricKitManager.shared.start()` を呼ぶだけです。
### MetricKitの特徴
- **外部送信なし** — データはアプリ内で完結。Appleのサーバーにも送らない
- **即時通知** — iOS 15以降、クラッシュ直後に `didReceive(_: [MXDiagnosticPayload])` が呼ばれる
- **バッテリー・パフォーマンス** — クラッシュだけでなく、起動時間やメモリ使用量も計測できる
### Xcode Organizer との違い
Xcode Organizer(Xcode → Window → Organizer → Crashes)も追加実装なしでクラッシュログを表示しますが、以下の制限があります。
- ユーザーが「診断データ共有」をオンにしている必要がある(オプトイン)
- ある程度のユーザー数がないとデータが集まらない
- 蓄積に数日〜数週間かかる
ユーザー数が少ない段階では MetricKit の方が実用的です。
## 2. App Store Connect Analytics API — アナリティクス
App Store Connect のWebコンソールでもアナリティクスは見られますが、APIで取得すると自動化や詳細な分析が可能になります。
### 準備:JWT認証
App Store Connect APIは JWT(JSON Web Token)で認証します。
```python
import jwt
import time
import requests
KEY_ID = "YOUR_KEY_ID"
ISSUER_ID = "YOUR_ISSUER_ID"
with open("AuthKey.p8", "r") as f:
private_key = f.read()
payload = {
"iss": ISSUER_ID,
"iat": int(time.time()),
"exp": int(time.time()) + 1200,
"aud": "appstoreconnect-v1"
}
token = jwt.encode(payload, private_key, algorithm="ES256",
headers={"kid": KEY_ID})
headers = {"Authorization": f"Bearer {token}"}
```text
### レポートリクエストの作成
Analytics APIを使うには、まずレポートリクエストを作成(または既存のものを取得)します。
```python
APP_ID = "YOUR_APP_ID"
# レポートリクエストの確認
r = requests.get(
f"https://api.appstoreconnect.apple.com/v1/apps/{APP_ID}/analyticsReportRequests",
headers=headers
)
report_requests = r.json()["data"]
# なければ作成(ONGOINGで継続的にレポート生成)
if not report_requests:
r = requests.post(
f"https://api.appstoreconnect.apple.com/v1/apps/{APP_ID}/analyticsReportRequests",
headers=headers,
json={
"data": {
"type": "analyticsReportRequests",
"attributes": {"accessType": "ONGOING"},
"relationships": {
"app": {"data": {"type": "apps", "id": APP_ID}}
}
}
}
)
```text
### レポートの取得
```python
import gzip
import csv
import io
from collections import defaultdict
REPORT_REQUEST_ID = report_requests[0]["id"]
def fetch_report(report_name):
"""指定したレポートの全インスタンスを取得して結合"""
# レポート一覧を取得
r = requests.get(
f"https://api.appstoreconnect.apple.com/v1/analyticsReportRequests/"
f"{REPORT_REQUEST_ID}/reports",
headers=headers, params={"limit": 200}
)
for report in r.json()["data"]:
if report["attributes"]["name"] == report_name:
# インスタンス(日付ごとのデータ)を取得
r2 = requests.get(
f"https://api.appstoreconnect.apple.com/v1/analyticsReports/"
f"{report['id']}/instances",
headers=headers, params={"limit": 10}
)
all_data = ""
for inst in r2.json().get("data", []):
# セグメント(実際のデータファイル)を取得
r3 = requests.get(
f"https://api.appstoreconnect.apple.com/v1/"
f"analyticsReportInstances/{inst['id']}/segments",
headers=headers
)
for seg in r3.json().get("data", []):
url = seg["attributes"].get("url")
if url:
r4 = requests.get(url)
text = gzip.decompress(r4.content).decode("utf-8")
lines = text.strip().split("\n")
if not all_data:
all_data = text.strip()
else:
all_data += "\n" + "\n".join(lines[1:])
return all_data
return None
```text
### 利用可能なレポートの例
| レポート名 | カテゴリ | 内容 |
|-----------|---------|------|
| App Downloads Standard | COMMERCE | ダウンロード数(新規/再DL、デバイス、地域別) |
| App Sessions Standard | APP_USAGE | セッション数、平均時間 |
| App Store Discovery and Engagement Standard | APP_STORE_ENGAGEMENT | インプレッション、ページ閲覧、タップ |
| App Store Installation and Deletion Standard | APP_USAGE | インストール・削除数 |
| App Crashes | APP_USAGE | クラッシュ数 |
### 実際の出力例
KotenOCRの公開直後のデータです。
```text
=== ダウンロード数 ===
2026-03-20: 新規 95 / 再DL 2 / 計 97
2026-03-21: 新規 348 / 再DL 14 / 計 362
2026-03-22: 新規 104 / 再DL 188 / 計 292
2026-03-23: 新規 23 / 再DL 231 / 計 254
合計: 新規 570 / 再DL 435 / 計 1,005
=== セッション数 ===
2026-03-20: 159 セッション / 平均 81秒
2026-03-21: 345 セッション / 平均 85秒
2026-03-22: 126 セッション / 平均 84秒
2026-03-23: 50 セッション / 平均 115秒
=== App Store エンゲージメント ===
2026-03-19: Impression: 79 / Page view: 14 / Tap: 19
2026-03-20: Impression: 172 / Page view: 315 / Tap: 336
2026-03-21: Impression: 596 / Page view: 576 / Tap: 600
2026-03-22: Impression: 193 / Page view: 106 / Tap: 101
=== インストール / 削除 ===
2026-03-20: インストール 63 / 削除 0
2026-03-21: インストール 249 / 削除 0
2026-03-22: インストール 66 / 削除 0
2026-03-23: インストール 56 / 削除 0
```text
データの遅延は約1〜2日です。Web UIではほぼ前日分まで閲覧できます。
## 3. Siren — アップデート促進
[Siren](https://github.com/ArtSabintsev/Siren) は、App Storeの最新バージョンとアプリのバージョンを比較し、更新を促すダイアログを表示するOSSライブラリです。
### 導入
Swift Package Manager で追加します。
```text
https://github.com/ArtSabintsev/Siren (6.1.0+)
```text
### 実装
```swift
import Siren
// SwiftUIの場合、onAppear内で呼ぶこと(init()では動かない)
private func configureSiren() {
let siren = Siren.shared
siren.rulesManager = RulesManager(
majorUpdateRules: .critical, // メジャー: スキップ不可
minorUpdateRules: .annoying, // マイナー: スキップ不可(後で通知)
patchUpdateRules: .default // パッチ: スキップ可能
)
siren.wail()
}
```text
### 注意点
- **SwiftUIでは `onAppear` で呼ぶ** — `App.init()` の段階ではUIWindowが存在しないため、Sirenが正しく動作しません
- **テスト方法** — Sirenは iTunes Lookup API で App Store上のバージョンと比較するため、テスト時は Info.plist の `CFBundleShortVersionString` を意図的に古いバージョンに設定する必要があります
- **リリース直後の遅延** — デフォルトでリリースから1日経過しないとダイアログが表示されません(`showAlertAfterCurrentVersionHasBeenReleasedForDays` で変更可能)
### ダイアログの種類
| ルール | 動作 |
|--------|------|
| `.critical` | 「Update」ボタンのみ。スキップ不可 |
| `.annoying` | 「Update」「Next time」の2ボタン。スキップ不可だが後回し可 |
| `.default` | 「Update」「Next time」「Skip」の3ボタン |
| `.relaxed` | `.default` と同じだが、表示頻度が低い |
## 4. SKStoreReviewRequest — レビュー促進
Apple標準のレビュー促進APIです。追加のフレームワークは不要です。
```swift
import StoreKit
// OCR成功後など、ユーザーが価値を感じたタイミングで呼ぶ
func requestReviewIfAppropriate() {
let count = UserDefaults.standard.integer(forKey: "ocrSuccessCount")
// 3回目のOCR成功後にレビューを依頼
if count == 3 {
if let scene = UIApplication.shared.connectedScenes
.first(where: { $0.activationState == .foregroundActive })
as? UIWindowScene {
SKStoreReviewController.requestReview(in: scene)
}
}
}
```text
### ポイント
- Appleがダイアログの表示を制御するため、呼び出しても必ず表示されるとは限らない
- **年3回まで**の制限がある
- ユーザーが「成功体験」を感じた直後に呼ぶのが効果的
## 5. 所感と限界
### この構成でカバーできること
- 基本的なダウンロード数・セッション数の把握
- クラッシュの検知と原因調査
- ユーザーへのアップデート促進
- レビュー促進
実際に KotenOCR をこの構成で運用していますが、個人開発アプリとしては十分な情報が得られています。
### この構成の限界
- **画面単位・機能単位の詳細な計測ができない** — 「どの画面が最も使われているか」「翻訳機能の利用率は?」といった分析にはアプリ内のイベント計測が必要
- **リアルタイム性がない** — Analytics APIのデータには1〜2日の遅延がある
- **MetricKitのクラッシュ情報は構造化されていない** — スタックトレースのJSON解析は自前で実装する必要がある
- **A/Bテストや段階的ロールアウトは不可能** — これらが必要な場合はFirebase Remote Configなどが必要
### Firebaseが必要になるケース
- ユーザー行動の詳細な分析が必要な場合
- リアルタイムのクラッシュ通知が必要な場合
- Feature Flagsやリモート設定が必要な場合
- プッシュ通知を実装する場合
## まとめ
| ツール | 用途 | 実装コスト |
|--------|------|-----------|
| MetricKit | クラッシュ・パフォーマンス計測 | 小 |
| App Store Connect Analytics API | DL数・セッション・エンゲージメント | 中(Python) |
| Siren | アップデート促進ダイアログ | 小 |
| SKStoreReviewRequest | レビュー促進 | 最小 |
| Xcode Organizer | クラッシュログ閲覧 | なし |
Firebase不要でも、個人開発アプリの運用に必要な情報はほぼ揃います。プライバシーポリシーの記載義務も最小限で済み、ユーザーに対して「データを収集していません」と言い切れるのは大きなメリットです。
まずはこの構成で始めて、必要に応じてFirebaseを追加する、という段階的なアプローチがおすすめです。
## 関連記事
- [KotenOCR:くずし字をオフラインで認識するiOSアプリの開発と公開](/posts/kotenocr-ios-app/)
- [App Store Connect APIだけでiOSアプリを審査提出する手順](/posts/appstore-connect-api-guide/)
- [iOSアプリのメモリ最適化とクラッシュ修正の実践記録](/posts/ios-memory-crash-fixes/)