HN 日本語サマリー

← 一覧へ戻る
プログラミング

セルフホスト型UmamiをiOSアプリ分析に利用する

Using self-hosted Umami for iOS app analytics (hjerpbakk.com)

51 pointsby Sankra7 コメント

要約

この記事では、セルフホスト型のWeb分析ツールUmamiを、IDFAプロンプトやサードパーティによるデータ収集を避けたいiOSアプリの分析に利用する方法を紹介しています。開発者は、軽量なSwiftパッケージ`umami-swift`を作成し、アプリをUmami上でウェブサイトとして登録することで、訪問者数や画面表示数などをWebサイトと同様のダッシュボードで追跡できるようになります。

全文翻訳

セルフホスト型UmamiをiOSアプリ分析に利用する 2026年7月14日 | 7分読了 | 分析, iOS, アプリ, オープンソース, Fly, Cloudflare Umamiをすでにセルフホストしているため、重い分析SDKやIDFAプロンプト、あるいは代わりにデータを収集するサードパーティなしで、アプリでも同じ体験を求めていました。これを実現するために、私は小さなオープンソースSwiftパッケージumami-swiftを作成しました。これにより、Webサイトと同じ訪問者数とセッション数を、同じダッシュボードで取得できます。 私のUmamiインスタンスにアプリも追加されました 各アプリはUmami上でウェブサイトとして登録され、実際のウェブサイトの隣のウェブサイトリストに表示されます。実際のアプリアイコンも含まれています。 アプリの概要は、訪問者数、セッション数、ページビュー数が同じようにカウントされ、ウェブサイトの概要と全く同じように表示されます。 アプリの画面はページとして報告されます。私のワークアウトトラッカーSimplest Workout Trackerでは、カレンダー、年間サマリー、設定シート、ワークアウト記録シートがそれぞれ表示されるため、どの部分が実際に使われているかがページリストに示されます。 どのように実現したか umami-swiftは、依存関係のないオープンソースのSwiftパッケージです。公開APIは意図的に小さくされています。 ```swift import Umami // 起動時: Umami.configure( websiteId: "your-website-id", host: "cardgame.ios", baseURL: URL(string: "https://your-umami-host")! ) // 任意の場所: Umami.track("game_started") Umami.track("level_completed", ["level": 7, "won": true]) // ユーザーが別の画面に移動したとき: Umami.screen("settings") // オプトアウト: Umami.setEnabled(false) ``` configureは、ウェブサイトIDとホスト文字列を受け取ります。ホストは、Umamiでウェブサイトを追加する際に使用するドメインです。アプリには実際のドメインがないため、simplestworkouttracker.iosのような疑似ドメインを使用しています。baseURLは、前の投稿で触れたFlyマシンであるhttps://hjerpbakk-analytics.fly.devに設定しています。Umamiを実行している他の人は、各自のインスタンスを指すようにします。設定により、起動時のページビューとapp_startedイベントがキューイングされるため、追加のコードなしでUmamiの概要に起動が表示されます。trackは、オプションの追加データ辞書を持つ名前付きイベントを送信します。screenは、名前付き画面のページビューを送信します。どちらもディスクにキューイングされ、バッチで送信されるため、ネットワークがない状態での起動でも何も失われず、UIが分析を待つことはありません。 アプリへの追加 自分のアプリに追加するには、いくつかの手順が必要です。 Xcodeで、File → Add Package Dependencies を選択し、https://github.com/hjerpbakk/umami-swift を貼り付け、ルールをUp to Next Major Version from 1.3.0 に設定し、Umamiプロダクトをアプリターゲットに追加します。Package.swiftプロジェクトの場合は、dependenciesに.package(url: "https://github.com/hjerpbakk/umami-swift", from: "1.3.0")を追加し、Umamiプロダクトをターゲットに追加します。 Umamiダッシュボードで、アプリの新しいウェブサイトを追加します。アプリには実際のドメインがないため、myapp.iosのような安定した疑似ドメインを付け、生成されたウェブサイトIDをコピーします。アプリのinit(またはapplication(_:didFinishLaunchingWithOptions:))から、起動時に一度Umami.configureを呼び出します。ウェブサイトID、Umamiに入力したのと同じドメインをホストとして、そして自身のUmamiインスタンスをbaseURLとして渡します。 ```swift Umami.configure( websiteId: "<your-website-id>", host: "myapp.ios", baseURL: URL(string: "https://your-umami-host")! ) ``` アプリを実行し、Umamiダッシュボードでそのウェブサイトを開きます。app_startedイベントが数秒以内に訪問者ID付きで表示され、イベントが流れていることを確認できます。これが統合の全てです。この記事の残りは、すべてがどのように機能するかの詳細です。 同じバックエンドのための異なるクライアント Umamiのトラッキングスクリプトは2つのエンドポイントと通信します: 単一イベント用の/api/sendと、複数イベント用の/api/batchです。どちらもJSONボディを受け取り、APIキーや署名付きリクエストを要求しません。Umamiは、送信者ではなく、ペイロード内のウェブサイトIDによってウェブサイトを識別します。そのため、Swiftアプリはブラウザやスクリプトタグなしで、同じエンドポイントに直接呼び出すことができます。トラッキングスクリプトが生成するものと同じ形状のJSONを送信し、Umamiのボットフィルターを回避するUser-Agentを使用します。前の投稿で触れたハニーポットはアプリにも適用されます。ボットのように見えるリクエストは200を返され、ダッシュボードにはヒットが表示されません。 ユニークユーザーの正確なカウント Umamiはトラフィックを2つの単位でカウントします: 訪問者(visitors)とセッション(visits)。訪問者とはセッションをまたいで持続するIDであり、セッションとは1回のセッションのことです。Webでは、UmamiはIPアドレスとUser-Agentのクッキーレスハッシュから訪問者を計算します。アプリにはハッシュするブラウザセッションがないため、umami-swiftは訪問者ID自体を提供します。各イベントはpayload.id、つまりUmamiが既に計算された訪問者識別子として受け入れるUUIDを運びます。このUUIDはランダムで、メモリ内にのみ存在し、カレンダーの日が変わるとローテーションします。デバイスには何も書き込まれないため、読み戻せるIDはなく、異なる日の使用をリンクすることはできません。IDはプロセス終了時に失われるため、実際には各起動が新しい訪問者としてカウントされ、典型的なライフタイムではなく1日を上限とします。これにより、アプリはUmamiのクッキーレスWebトラッキングと同じように、訪問者が保存された識別子ではなく、サーバーサイドでローテーションするハッシュであるという位置づけを保ちます。ここで行われることはIDFAやApp Tracking Transparencyには触れず、IDは誰かをアプリ、インストール、あるいは日をまたいで追跡することはできません。 ページビューが概要を機能させる Umamiはページビューから訪問者数、セッション数、ビュー数を計算します。名前付きペイロードはカスタムイベントであり、ページビューではありません。そのため、名前付きイベントだけでは概要はゼロのままになり、イベントの下にのみ表示されます。したがって、クライアントは起動ごと、およびフォアグラウンドに戻るたびに/に対してページビューを送信します。通信上では、隣接するapp_startedイベントとほぼ同じペイロードですが、ページビューには名前がありません。これにより、概要に実際の数値が表示されます。起動ごとのビューが1つ、セッション数と訪問者数はウェブサイトの場合と同じようにカウントされます。画面もオプトインできます。Umami.screen("settings")は/settingsのページビューを送信し、画面はUmamiのページリストにウェブサイトのページのように表示されます。これが記事の冒頭で示されたページリストを埋めるものです。 広告ブロッカーの障害なし 前の投稿の多くはCloudflare /np/パスについてでしたが、ブラウザは訪問者がインストールした広告ブロッカーやプライバシ拡張機能をすべて介して各リクエストをルーティングします。そこではブロッカーが完全なURLを見て、EasyPrivacyのようなブロックリストがUmamiの通常のホスト名やスクリプトパスに一致します。/np/パスが回避するのはこれです。アプリはブロックされにくいです。iOSで一般的なSafariコンテンツブロッカーは、Safari内でのみ機能し、ネイティブアプリのトラフィックを見ることはありません。1Blockerのファイアウォールのようなシステム全体ブロッカー、またはNextDNSやAdGuardのようなDNSブロッカーは異なります。これらはオンデバイスVPNまたはリゾルバとして実行され、アプリとネットワークの間に配置されます。しかし、それはDNSルックアップとTLSハンドシェイクのサーバー名からドメインしか見ません。パスは暗号化されたリクエスト内に留まるため、ブラウザでUmamiを捕捉するパスとスクリプト名のルールは一致するものがなく、残るのはホスト名だけです。hjerpbakk-analytics.fly.devのような目立たないドメインのセルフホストインスタンスは、ブロックリストに載っていないため、リクエストは通過します。その正確なドメインを手動でカスタムリストに追加すると停止しますが、それ自体では何も停止しません。そのため、umami-swiftはCloudflare Workerを経由せず、Flyオリジンに直接話しかけます。/np/プロキシはウェブサイトに対してその役割を果たしますが、アプリはそれをスキップします。ただし、これからわかるように、それは依然としてその役割を果たします。 Umamiでのアプリアイコンの表示 Umamiはダッシュボードの各ウェブサイトの隣に小さなアイコンを表示します。これは登録されたドメインに基づいてDuckDuckGoのファビコンサービスから取得されます。これは実際のウェブサイトでは機能します。私のアプリはsimplestworkouttracker.iosのような疑似ドメインで登録されており、どのファビコンサービスも解決できないため、すべてのアプリが同じ灰色の地球儀になりました。セルフホストということは、これを変更できるということです。UmamiはFAVICON_URL環境変数を読み込み、{{domain}}プレースホルダーを持つURLテンプレートでファビコンサービスをスワップします。私のものは前の投稿のCloudflare Workerを指しています。 ``` FAVICON_URL=https://hjerpbakk.com/icons/{{domain}} ``` そのため、アプリはトラッキングのためにWorkerをスキップしますが、結局1つの小さな仕事をすることになりました。