FCMを用いたプッシュ通知の管理方針
Status: Accepted
要約
- アカウントID、登録トークン、登録日時の組をバックエンドサーバのデータベースに保存します。
- アカウント情報取得APIで、そのアカウントに保存されている登録トークンを返します。ただし、登録日時から1か月経過している登録トークンは返却しません。
- ログイン(自動ログイン含む)後のアカウント情報取得後に、現在の登録トークンがアカウントに保存されていなければトークン更新APIを通じて保存します。
- ログイン(自動ログイン含む)後のアカウント情報取得後に、現在の登録トークン登録日時から1か月経過している場合は登録トークンを再登録します。
- ログアウト時にトークン削除APIを通じて登録トークンを削除します。
- FCMから無効なトークンのエラー応答が返ってきた場合は登録トークンを削除します。
コンテキスト
FCMを通じてプッシュ通知を送信する場合、宛先を示す各デバイスの登録トークンをどのように管理するかが重要な課題となります。 また、送信レートについての考慮であったり、FCMからのエラー応答にどのように対応するかも設計しておく必要があります。
ここでは次の観点でFCMを用いたプッシュ通知の管理方針について議論・調査を進めていきます。
- 登録トークンの管理
- レート制限
- エラーハンドリング
議論
登録トークンの管理
FCM登録トークン管理のベストプラクティスが公式のドキュメントとして用意されています。 このアプリでも基本的にはこのベストプラクティスに従うものとします。
登録トークンの登録(バックエンド側)
プッシュ通知方式の方針に従い、 このアプリでは、各ユーザの各デバイスの登録トークンをバックエンドサーバのデータベースで管理します。 バックエンドサーバはトークン更新APIを通じてクライアントアプリから登録トークンを受け取り、その登録トークンをデータベースに保存します。
また、FCM登録トークン管理のベストプラクティスに従い、登録トークンの登録日時のタイムスタンプもあわせてデータベースに保存します。 これは、使われなくなった古い登録トークンがどれなのか判別できる情報をバックエンド側に保持しておくことが目的です。 定期的にクライアントアプリ側で登録トークンを再登録しなおし、タイムスタンプを更新することで、 アプリが利用されなくなった登録トークンのタイムスタンプだけ古い日時のままとなります。 これにより、アプリが起動されなくなったデバイスをプッシュ通知の送信対象から除外するような対応が可能になります。 これは、大量のデバイスへ通知を送信するユースケースでのレート制限の回避やパフォーマンス改善につながります。
上記を踏まえ、データベースには、アカウントID、登録トークン、登録日時の組を保存するテーブルを用意して保存します。
具体的なバックエンドサーバ側の処理の流れは以下のとおりです。
- クライアントアプリからAPIを通じて登録トークンを受け取る
- 認証情報からアカウントを特定し、対象アカウントに対して登録トークンがデータベースに保存されていなければ現時刻を登録日時としてトークンとともに保存する
登録トークンの登録(クライアント側)
デフォルトでは、Firebase SDKはアプリの起動時にクライアントアプリの登録トークンを生成します。 またそのタイミングでライブラリによりそのIDと構成データがFirebaseにアップロードされます。 このアプリでも、登録トークンの生成とFirebaseへの登録はFirebase SDKのデフォルトにあわせてアプリの起動時に行います。 この時点でFirebaseへのデバイス登録は行われますが、まだバックエンドサーバ側では登録トークンを保持していないため、通知の送信対象にはしません。
このアプリではログインしていないデバイスへは通知を送信しません。 そのため、バックエンドサーバへはログイン中のデバイスの登録トークンのみを登録します。 そのためアプリ起動時ではなく、デバイス側でログインが完了した後にバックエンドサーバへ登録トークンを送信します。
具体的なクライアント側の処理の流れは以下のとおりです。
- ユーザによる手動ログイン、または自動ログイン処理の完了後に、バックエンドサーバからアカウントに対応する登録トークン一覧を取得する
- Firebase SDKを通じて取得した現在の登録トークンが一覧に含まれているか確認する
- 一覧に含まれていない場合は、バックエンドサーバに現在の登録トークンを登録する
一覧に含まれていない理由としては、新規登録の場合と定期更新の場合があります。 定期更新の場合、バックエンドサーバ側は現在の登録トークンのタイムスタンプを更新する流れとなります。
iOS端末の場合、登録トークンを利用して通知を送信するためには、このアプリからの通知の送信を許可するか確認するダイアログを表示し、許可を取る必要があります。 ログインが完了したタイミングでこの確認が一度も実施されていない場合、確認ダイアログを表示します。 このアプリではこれは通常、サインアップ完了後の初回ログイン完了時となります。
iOS端末の場合、通知機能を利用する前にアプリ内で通知送信の許可を求めるダイアログを表示し、ユーザの意思を確認する必要があります。 それをユーザが拒否した場合、以降アプリ内では許可を求めるダイアログを再表示できず、 ユーザがOSの設定からアプリの通知をオンに設定しない限り通知機能は利用できません。
しかし、通知が何の目的で用いられるのか説明される前に許可を求めるダイアログが表示されると、許可しないを選択するユーザも少なくありません。 これはユーザの体験を向上させる目的で通知機能を提供しているアプリにとって不幸なすれ違いです。
このアプリではログイン直後に説明なく通知の許可を求めるダイアログを表示していますが、 実際のアプリケーションではよりユーザが納得しやすい形で許可を求めるべきです。 この問題を改善するために、例えば以下のような方法を採用できます。
プッシュ通知が本当に必要になるタイミングで通知許可を要求する
例えば、特定の条件を満たした場合に通知を受け取るという設定がアプリ内に存在するとします。 この設定をオンに設定したタイミングで通知許可ダイアログを表示した場合、ユーザはこの通知を受け取るために通知の許可を要求しているのだと明確に理解できます。
プッシュ通知の目的を説明する画面を用意し、その画面で通知の目的に同意したユーザにのみiOSの提供する通知許可ダイアログを表示する
iOSが用意している通知許可ダイアログは一度しか表示できません。 そのため何度でも表示できる別の画面で一度ユーザの意向を確認した上で、同意してくれたユーザにのみiOSの提供する通知許可ダイアログを表示します。 用意した画面で同意しなかったユーザに対しても、後からいつでもその画面に遷移して通知を許可できるようにします。
アプリからOSの通知設定画面へ誘導する
一度iOSの通知許可ダイアログで拒否された場合、OSのアプリ設定の画面からしか通知設定は変更できません。 通知を有効化する方法をアプリ内で説明し、アプリ設定画面を開くためのボタンなどを用意しておくことで、設定を変更してもらえる可能性が高まります。 ただし、ユーザの意思を尊重せずに通知の有効化を強要するようなデザインにしてしまうと、ストア公開審査時に規約違反と判断される可能性があります。
Provisional Authorizationを利用する
iOS 12から、通知許可ダイアログでの明示的な許可を得る前に仮許可状態で通知を表示する、 Provisional Authorizationの機能が追加されました。 この状態で受信した通知は通知音の再生やバナー表示、ロック画面への表示などは行われず、通知センターにのみ表示されます。 また、この状態で受信した通知には、このアプリからの通知を継続して表示するか拒否するかのボタンが付属します。 ユーザは実際に送られてきた通知の内容を見てから、通知を許可するかどうかを判断できます。
登録トークンの更新
デバイスの登録トークンは以下の場合などに変更される可能性があります。
- アプリが元のデバイスから新しいデバイスに復元される
- ユーザがアプリをアンインストールまたは再インストールする
- ユーザがアプリデータをクリアする
初回起動時と同様、この場合もFirebase SDKはデフォルトでは次回のアプリ起動時に登録トークンを生成します。 またそのタイミングでライブラリによりそのIDと構成データがFirebaseにアップロードされます。
このアプリでは、現在の登録トークンがバックエンドサーバに登録されているかをログイン(自動ログイン含む)完了後に毎回確認します。 そのため、登録トークンが変更されていた場合も未登録と判断され、次回ログイン(自動ログイン含む)完了後にバックエンドサーバへ再登録されます。 実施する処理はクライアント側、バックエンド側ともに初回登録時と同様です。
iOSの通知送信許可を求めるダイアログも、再インストールなどにより再表示が必要な状態になっていれば、このタイミングで表示します。
また登録トークンが無効化された場合だけでなく、登録トークンの登録日時をもとに定期的に登録トークンを更新します。 FCMでは、月に1度程度登録トークンとタイムスタンプを更新することを推奨しています。 このアプリでもそれに倣い、月に1度登録トークンとタイムスタンプを更新します。 ベストプラクティスではこの情報をもとに、2か月以上更新されていない古い登録トークンのトピック購読解除を行うことを推奨しています。 このアプリではトピック購読機能は利用しないため、この棚卸しまでは実施しません。
このアプリでは、登録トークンの定期更新は、バックエンドサーバがアカウントに対応する登録トークン一覧を返す際に、 レスポンスに登録から1か月以上経過した古い登録トークンを含めないことで実現します。 クライアント側は取得した一覧に現在の登録トークンが含まれていなかった場合、タイムスタンプを更新する為、現在の登録トークンをバックエンドサーバに再登録します。
登録トークンの削除
FCMは、登録トークンを対象とした通知送信リクエストを受け取った場合、それが無効な登録トークンであれば以下のエラー応答を返します。
- UNREGISTERD (HTTP 404)
- 指定した登録トークンがそのアプリに対して登録されていない場合に返されます
- INVALID_ARGUMENT (HTTP 400)
- リクエストパラメータに問題がある場合に返されます
- 登録トークンが問題の原因である場合は、Invalid registrationという理由が返されます
無効な登録トークンであることがわかった場合、次回以降に送信対象としないよう、登録トークンをデータベースから削除することが望まれます。
このアプリでは、通知の送信時にUNREGISTERDのエラーを受け取った場合には、無効な登録トークンをデータベースから削除します。 また、登録トークンをデータベースへ保存する前に、通知を送信せず検証のみを行うよう、validate_onlyのパラメータをtrueにしてFCMへリクエストを送信します。 ここでINVALID_ARGUMENTのエラーを受け取った場合には、登録トークンをデータベースへ保存しないようにします。
また、今回のこのアプリではログインしている端末にのみ通知を送信したいため、ログアウト時にも登録トークンを削除する必要があります。 ログアウト時の登録トークン削除は、ログアウトのAPIで登録トークンをあわせて送信する方法と、別途登録トークン削除用のAPIを用意する方法が考えられます。 このアプリではログアウトのAPIとは別に、トークン削除APIを通じて登録トークンを削除するものとします。
その他の削除タイミングとしては、アプリの設定で通知設定がOffに変更されたタイミングも考えられます。 このタイミングで削除する場合、次に通知設定がOnに変更されたことを検出し、そのタイミングで登録トークンをバックエンドサーバに再保存する必要があります。 このアプリでは、処理が複雑になることを避けるため、通知設定がOffに変更されたタイミングでは登録トークンを削除しません。
レート制限
FCMでは、プッシュ通知の送信レートの制限が設けられています。 また、FCMを介して利用するAPNsでも、直接公式ドキュメントでの言及はありませんが制限が設けられていると思われます。 それぞれの詳細は以下のとおりです。
FCMが定める上限
FCMにおけるプッシュ通知の送信レート制限については、 FCM メッセージについて のドキュメント内に記載されています。
主な制限は以下のとおりです。
- 1台のデバイスに送信できるメッセージ
- 1分あたり最大240件
- 1時間あたり最大5000件
- アップストリームメッセージの制限
- プロジェクトあたり1500000件/分
- デバイスあたり1000件/分
- トピック・デバイスグループを対象とした送信(ファンアウト)
- プロジェクトあたりの同時ファンアウト数1000
- トピック登録・解除の制限
- トピック登録の追加・解除はプロジェクトごとに3000 QPS
これを超える送信レートで送信リクエストを送った場合は、FCMから429 QUOTA_EXCEEDEDのエラー応答が返されます。
このアプリにおける通知のユースケースでは、いずれも通知の送信頻度や送信対象台数は多くないため、 基本的にはこれらの送信レートに抵触することはないと考えられます。 FCMから429 QUOTA_EXCEEDEDが返ってきた場合には、後述するエラーハンドリングの再送制御の方針に従います。
APNsが定める上限
通知に関する送信上限について明確に記載された最新のドキュメントは見つけられませんでした。
Appleのドキュメントアーカイブに残されている過去のドキュメントとしては、 Troubleshooting Push Notificationsがありました。 ここでは、"There are no caps or batch size limits for using APNs."と記載されています。
しかしAPNsが返しうるステータスコードの中には429が含まれています。 その理由としては、"The server received too many requests for the same device token."と記載されています。 そのため、同一のデバイスにあまりにも高頻度に送信すると制限されることが予想されます。
こちらも同様に、このアプリにおける通知のユースケースでは送信上限に抵触することはないと考えられます。 APNsから429 TooManyRequestsの応答が返ってきた場合には、後述するエラーハンドリングの再送制御の方針に従います。
エラーハンドリング
エラー応答の種類
FCMへの通知送信リクエストの詳細は、 こちらの公式ドキュメントに記載されています。
Firebase Admin SDKを用いている場合、レスポンスからエラーコードを取得できます。 FCMのエラーコードの詳細は、ErrorCode のドキュメント内に記載されています。
APNs起因のエラーの詳細については、 Handling Notification Responses from APNs のドキュメント内に記載されています。
再送制御
FCMから以下のエラー応答が返ってきた場合には、通知送信の一時的な失敗とその理由をログに記録した上で、時間をおいた後にFCMへの通知送信をリトライします。
- 429 QUOTA_EXCEEDED
- 503 UNAVAILABLE
- 500 INTERNAL
リトライするまでの待機時間は、以下のルールに従って決定する必要があります。
- レスポンスヘッダにRetry-Afterが含まれている場合は、そのヘッダで指定された秒数だけ待機
- レスポンスヘッダにRetry-Afterが含まれていない場合は指数バックオフに従い、1秒、2秒、4秒と指数関数的に時間を増やしながら待機
このアプリでは、クライアント側となるモバイルアプリのサンプルを充実させることに注力しているため、Firebase Admin SDKが自動的にリトライする仕様に準拠します。
Firebase Admin SDKのRetry-Afterサポートには以下の記載があります。
Go
や.Net
のAdmin SDKはRetry-AfterのサポートがされているPython
のAdmin SDKはRetry-Afterのサポートがされていない
上記以外の言語に関してはドキュメントに仕様が記載されていないため、各言語のAdmin SDKのソースコードをみて仕様を確認する必要があります。
送信に成功しなかった場合には、通知送信の失敗とその理由をログに記録します。 その後の処理については、通知のユースケースごとにアプリケーションのエラーハンドリングとして別途検討します。
無効な登録トークンの削除
通知の送信時にFCMから以下のエラー応答が返ってきた場合には、通知送信の失敗とその理由をログに記録した上で、バックエンドサーバに保存されている当該登録トークンを削除します。 その後の処理については、通知のユースケースごとにアプリケーションのエラーハンドリングとして別途検討します。
- 404 UNREGISTERED
400 INVALID_ARGUMENTについては、登録トークンをデータベースへ保存する前に検証しています。 そのため実際の通知送信時にはInvalid registrationを理由とした400 INVALID_ARGUMENTは返ってこない想定です。 400 INVALID_ARGUMENTに対しては、その他のエラーへの対応と同様に対応します。
その他のエラーへの対応
その他のエラー応答が返ってきた場合には、通知送信の失敗とその理由をログに記録します。 その後の処理については、通知のユースケースごとにアプリケーションのエラーハンドリングとして別途検討します。
運用上の注意点
APNs Auth Keyの管理
APNs Auth Keyを使った認証形式は、過去の証明書を用いた形式と異なり有効期限はなく、更新運用は不要です。 その代わり、APNs Auth KeyはApple Developer Programのアカウント全体で2つまでしか発行できず、 アプリ単位ではなくアカウント全体で共用されるものとなります。
2つまでしか発行できず、APNs Auth Keyの移行期間中に並行運用することなどを考えると 開発・本番環境で別の鍵を利用するのも難しいです。 鍵ファイルの管理には最新の注意を払いましょう。
決定
このアプリでは、アカウントID、登録トークン、登録日時の組をバックエンドサーバのデータベースに保存します。 アカウント情報取得APIで、そのアカウントに保存されている登録トークンを返すようにAPIを改修します。 この時、定期的な登録トークンの更新を促すため、登録日時が1か月以上前の登録トークンはレスポンスに含めません。
ログイン(自動ログイン含む)後のアカウント情報取得後に、現在の登録トークンがアカウントに保存されていなければトークン更新APIを通じて保存します。
ログアウト時にトークン削除APIを通じて登録トークンを削除します。 FCMから無効なトークンのエラー応答が返ってきた場合は登録トークンを削除します。 その他のエラーについては、通知送信の失敗とその理由をログに記録した上で、通知のユースケースごとにその後のハンドリング内容を検討します。