FCM 登録トークン管理のベスト プラクティス

FCM API を使用してプログラムで送信リクエストを作成する際に、時間の経過とともに、古い登録トークンを使用して非アクティブなデバイスにメッセージを送信することによってリソースが無駄になる場合があります。この状況は、Firebase コンソールで報告されるメッセージ配信データや BigQuery にエクスポートされるデータに影響し、配信率の大きな(ただし実際は正しくない)低下として示されることがあります。このガイドでは、メッセージ ターゲティングを効率化し、有効な配信レポートを生成するのに役立つ、いくつかの手法について説明します。

古い登録トークンと有効期限切れの登録トークン

期限切れの登録トークンは、FCM に 1 か月以上接続されていない非アクティブなデバイスに関連付けられたトークンです。時間が経過するにつれて、デバイスが FCM に再び接続する可能性は低くなります。これらの古いトークンのメッセージ送信とトピック ファンアウトは、配信される可能性はほとんどありません。

トークンが古くなる理由はいくつかあります。たとえば、トークンが関連付けられているデバイスが紛失、破損、保管されて忘れられている場合があります。

古いトークンが 270 日間使用されていない場合、FCM はトークンを期限切れトークンと見なします。トークンの有効期限が切れると、FCM はトークンを無効としてマークし、トークンへの送信を拒否します。ただし、デバイスが再び接続してアプリが開かれるまれなケースでは、FCM はアプリ インスタンスに新しいトークンを発行します。

基本的なベスト プラクティス

FCM API を使用してプログラムで送信リクエストを作成するアプリでは、いくつかの基本的なプラクティスに従う必要があります。主なベスト プラクティスは次のとおりです。

  • FCM から登録トークンを取得し、サーバーに保存します。このサーバーの重要な役割は、各クライアントのトークンを追跡し、アクティブなトークンの最新リストを保持することです。コードとサーバーにトークンのタイムスタンプを実装し、このタイムスタンプを定期的に更新することを強くおすすめします。
  • トークンの新鮮さを維持し、古いトークンを削除します。FCM が有効と見なさなくなったトークンを削除するだけでなく、トークンが古くなっていることを示す他の兆候をモニタリングして、事前に削除することもできます。このガイドでは、これを行うためのオプションをいくつか説明します。

登録トークンを取得して保存する

アプリを初めて起動すると、クライアント アプリのインスタンスの登録トークンが FCM SDK によって生成されます。このトークンは、API によるターゲット設定された送信リクエストに含める必要があります。また、トピックのターゲティングを行う際にトピック登録に追加する必要があります。

アプリは最初の起動時にこのトークンを取得し、タイムスタンプと一緒にトークンをアプリサーバーに保存することを強くおすすめします。このタイムスタンプは FCM SDK では提供されないため、自らのコードとサーバーによって実装する必要があります。

また、トークンをサーバーに保存し、次のような変更時にタイムスタンプを更新することも重要です。

  • アプリを新しいデバイスで復元した場合
  • ユーザーがアプリをアンインストールまたは再インストールした場合
  • ユーザーがアプリのデータを消去した場合
  • FCM の既存のトークンが期限切れになると、アプリは再びアクティブになります。

例: トークンとタイムスタンプを Cloud Firestore に保存する

たとえば、Cloud Firestore を使用して、fcmTokens というコレクションにトークンを保存できます。コレクション内の各ドキュメント ID はユーザー ID に対応しており、ドキュメントには現在の登録トークンとその最終更新タイムスタンプが保存されます。Kotlin の次の例に示すように、set 関数を使用します。

    /**
     * Persist token to third-party servers.
     *
     * Modify this method to associate the user's FCM registration token with any server-side account
     * maintained by your application.
     *
     * @param token The new token.
     */
    private fun sendTokenToServer(token: String?) {
        // If you're running your own server, call API to send token and today's date for the user

        // Example shown below with Firestore
        // Add token and timestamp to Firestore for this user
        val deviceToken = hashMapOf(
            "token" to token,
            "timestamp" to FieldValue.serverTimestamp(),
        )
        // Get user ID from Firebase Auth or your own server
        Firebase.firestore.collection("fcmTokens").document("myuserid")
            .set(deviceToken)
    }

トークンが取得されるたびに、sendTokenToServer を呼び出して Cloud Firestore に保存されます。

    /**
     * Called if the FCM registration token is updated. This may occur if the security of
     * the previous token had been compromised. Note that this is called when the
     * FCM registration token is initially generated so this is where you would retrieve the token.
     */
    override fun onNewToken(token: String) {
        Log.d(TAG, "Refreshed token: $token")

        // If you want to send messages to this application instance or
        // manage this apps subscriptions on the server side, send the
        // FCM registration token to your app server.
        sendTokenToServer(token)
    }
        var token = Firebase.messaging.token.await()

        // Check whether the retrieved token matches the one on your server for this user's device
        val preferences = this.getPreferences(Context.MODE_PRIVATE)
        val tokenStored = preferences.getString("deviceToken", "")
        lifecycleScope.launch {
            if (tokenStored == "" || tokenStored != token)
            {
                // If you have your own server, call API to send the above token and Date() for this user's device

                // Example shown below with Firestore
                // Add token and timestamp to Firestore for this user
                val deviceToken = hashMapOf(
                    "token" to token,
                    "timestamp" to FieldValue.serverTimestamp(),
                )

                // Get user ID from Firebase Auth or your own server
                Firebase.firestore.collection("fcmTokens").document("myuserid")
                    .set(deviceToken).await()
            }
        }

トークンの鮮度を維持し、古いトークンを削除する

トークンの鮮度の判断は簡単ではありません。すべてのケースに対応できるように、トークンが古くなったとみなされるしきい値を採用する必要があります。デフォルトでは、アプリ インスタンスが 1 か月間接続されていない場合、FCM はトークンを古いと見なします。トークンが 1 か月以上経過した場合、そのデバイスは非アクティブである可能性が高いです。デバイスがアクティブであれば、その間にトークンを更新しているでしょう。

ユースケースによっては、1 か月が短すぎる場合や長すぎる場合があります。適切な基準はお客様が判断してください。

FCM バックエンドからの無効なトークン レスポンスを検出する

FCM からの無効なトークンのレスポンスを検出し、無効なことが判明した登録トークンをシステムから削除するようにします。HTTP v1 API では、次のエラー メッセージが返された場合、ターゲット設定された送信リクエストで無効なトークンまたは期限切れのトークンが使用されている可能性を示しています。

  • UNREGISTERED(HTTP 404)
  • INVALID_ARGUMENT(HTTP 400)

メッセージ ペイロードが有効で、対象のトークンに対してこれらのレスポンスのいずれかを受け取った場合は、そのトークンのレコードが有効になることはないため、削除しても問題ありません。たとえば、Cloud Firestore から無効なトークンを削除するには、次のように関数をデプロイして実行します。

    // Registration token comes from the client FCM SDKs
    const registrationToken = 'YOUR_REGISTRATION_TOKEN';

    const message = {
    data: {
        // Information you want to send inside of notification
    },
    token: registrationToken
    };

    // Send message to device with provided registration token
    getMessaging().send(message)
    .then((response) => {
        // Response is a message ID string.
    })
    .catch((error) => {
        // Delete token for user if error code is UNREGISTERED or INVALID_ARGUMENT.
        if (errorCode == "messaging/registration-token-not-registered") {
            // If you're running your own server, call API to delete the
            token for the user

            // Example shown below with Firestore
            // Get user ID from Firebase Auth or your own server
            Firebase.firestore.collection("fcmTokens").document(user.uid).delete()
        }
    });

FCM は、トークンが 270 日後に期限切れになった場合、またはクライアントが明示的に登録解除した場合にのみ、無効なトークン レスポンスを返します。独自の定義に従って古い状態をより正確に追跡する必要がある場合は、事前に古い登録トークンを削除できます。

トークンを定期的に更新する

サーバー上のすべての登録トークンを定期的に取得して更新することをおすすめします。 これには、以下のことが必要です。

  • クライアント アプリにアプリロジックを追加して、適切な API 呼び出し(Apple プラットフォームでは token(completion):、Android では getToken() など)を使用して現在のトークンを取得します。次に、現在のトークンをアプリサーバーに送信し、タイムスタンプとともに保存します。これは、すべてのクライアントまたトークンを対象にした月次ジョブになります。
  • トークンが変更されたかどうかにかかわらず、トークンのタイムスタンプを定期的に更新するためのサーバー ロジックを追加します。

WorkManager を使用してトークンを更新する Android ロジックの例については、Firebase ブログの Cloud Messaging トークンの管理をご覧ください。

どのようなタイミング パターンを使用しているかに関係なく、トークンは定期的に更新してください。1 か月に 1 回の更新頻度は、バッテリーへの影響と非アクティブな登録トークンの検出のバランスを取るのに最適です。この更新により、非アクティブになったデバイスが再びアクティブになったときに、登録が更新されるようにできます。更新頻度を週 1 回以上にする利点はありません。

古い登録トークンを削除する

デバイスにメッセージを送信する前に、デバイスの登録トークンのタイムスタンプが未更新時間枠内であることを確認してください。たとえば、Cloud Functions for Firebase を実装して毎日のチェックを実行し、タイムスタンプが定義済みの未更新時間枠(const EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 30; など)内であることを確認してから、古くなったトークンを削除します。

exports.pruneTokens = functions.pubsub.schedule('every 24 hours').onRun(async (context) => {
  // Get all documents where the timestamp exceeds is not within the past month
  const staleTokensResult = await admin.firestore().collection('fcmTokens')
      .where("timestamp", "<", Date.now() - EXPIRATION_TIME)
      .get();
  // Delete devices with stale tokens
  staleTokensResult.forEach(function(doc) { doc.ref.delete(); });
});

古いトークンをトピックから登録解除する

トピックを使用している場合は、古いトークンを、トピックから登録解除することもできます。この処理は 2 つの手順で行います。

  1. アプリで、月に 1 回、および登録トークンが変更されたときに、トピックに再登録するようにします。これは、アプリが自分自身を修復するソリューションとなり、アプリが再びアクティブになると登録が自動的に再開されます。
  2. アプリ インスタンスが 1 か月間(または独自の鮮度期間)アイドル状態である場合は、Firebase Admin SDK を使用してトピックから登録を解除し、トークンとトピックのマッピングを FCM バックエンドから削除するようにします。

この 2 つの手順の利点は、ファンアウト対象に含まれる古いトークンが少なくなるためにファンアウトが高速になること、古いアプリ インスタンスが再びアクティブになると自動的に再登録されることです。

配信の成功を測定する

メッセージ配信を最も正確に把握するには、アクティブに使用されているアプリ インスタンスにのみメッセージを送信することをおすすめします。登録者の多いトピックに定期的にメッセージを送信している場合、これは特に重要です。ある程度の割合で非アクティブになる登録者が発生すると、配信の統計データへの影響は時間とともに増大していきます。

トークンをターゲットにしたメッセージ送信を行う前に、次の点を考慮してください。

  • Google アナリティクス、BigQuery で取得されたデータ、または他のトラッキング シグナルによって、トークンがアクティブであることが示されているか。
  • 一定期間、連続して配信に失敗したか。
  • 過去 1 か月の間にサーバーの登録トークンが更新されました。
  • Android デバイスの場合、FCM Data API によって、droppedDeviceInactive によるメッセージ配信エラーの割合が高いと報告されるか。

配信の詳細については、メッセージ配信についてをご覧ください。