TUNAGのWeb Push通知基盤をFirebase Cloud Messagingへ移行しました

目次

はじめに

こんにちは。スタメンでTUNAGのバックエンド開発を行なっているきいろです。

TUNAGは組織活動を支援するサービスで、Webアプリとモバイルアプリの両方で提供しており、Webアプリではユーザーアクションをリアルタイムに届けるためのWeb Push通知機能を備えています。 TUNAGの代表的な機能として「制度」があり、そこから社内報など情報共有のための各種コンテンツ投稿が行えます。

それら投稿のリアルタイム通知を行うなど、Web Push通知機能はプロダクトのユーザー体験に直結する重要な仕組みとなっています。

今回、そのWeb Push通知機能について、Firebase Cloud Messaging(以下、FCM)をベースに移行を行いました。 この記事では、移行当時の調査内容や、バックエンドおよびフロントエンドの両面で直面した課題、そこから得られた知見を紹介します。 同様にFCMを用いたWeb Push通知の導入や移行を検討されている方にとって、何かしら参考になれば嬉しいです。

移行背景

TUNAGでは長らくPushCrew(現 VWO Engage)を利用し、Web Push通知機能を提供していました。 PushCrewはWeb Push通知機能を手軽にサービスに導入できる一方、長期運用をする中で通知用トークン管理や料金面などの課題が上がっていました。 また、通知設定をPushCrewから提供されるサブドメイン上で行うため、通知の再設定フローがやや複雑になりやすいなど、運用面での細かい課題も表面化していました。

移行先としてFCMを選んだ理由は、すでにモバイルアプリの通知基盤として活用しており、開発環境ごとにFirebaseプロジェクトが存在していたこと、認証まわりの仕組みも整っていたことが大きいです。 新規構築する部分が比較的少なく、既存リソースを活かしながらWeb側の通知基盤も統合できる点が、移行先への決め手となりました。

Web Push通知の仕組み

PushCrewからFCMへの移行にあたり、最も大変だったものがFCMの導入設計です。

当時の自分はWeb Push通知に関する知識が充分とは言えず、既存機能の連携構造も把握しきれていなかったため、Web Push通知送信に関する既存実装、PushCrew APIの仕様と利用形式を主軸に、地道にFCMの導入方針を定めていきました。

そもそもとしてのFCMベースでのWeb Push通知の仕組みについてですが、ブラウザ含めて以下4つの主要コンポーネントによって成立します。

コンポーネント 役割
Browser Web Push通知の受信
Push Service 通知の中継サービスその1。通知先エンドポイント管理。ブラウザに直接Web Push通知を届ける
FCM 通知の中継サービスその2。アプリケーション側のリクエスト起点でWeb Push通知を送信する
Application Web Push通知の送信

実際にWeb Push通知を行う場合は、(1)トークン作成→(2)Push通知送信の2ステップで行います。トークンとはWeb Push通知配信サービスを利用する際に、サービス事業者側でクライアント管理するために個別に発行するIDのことを表現しています。ID名称は通知配信サービス毎に異なりますが、この記事ではまとめて「トークン」と表記します。

Web Push通知を行う際の、具体的なコンポーネント間の連携フローは以下の通りです。

  • 青線:トークン作成
  • 赤線:Push通知送信

Web Push通知の処理フロー

Push通知先のエンドポイントの発行管理はPush Service(Push Subscription)で行い、FCMはdevice tokenを通じてPush Subscriptionを管理します。 実際の通知送信時は、アプリケーション側はFCM APIを通じてdevice tokenをFCM側に連携し、FCMはdevice tokenをもとに対応するPush Subscriptionを内部で参照、その後Push Serviceを経由してWeb Push通知の送受信が行われます。

ちなみにですが、FCMを通知配信基盤として利用する場合、Firebase SDKがこれらを抽象化してくれるため、Web Push通知の内部構造やPush Serviceの存在を強く意識する必要はありません。 実際の実装手順としては以下のようなシンプルなものになります。

  1. Service Workerの登録
  2. Firebase SDKから提供される getToken() を用いてdevice tokenを取得
  3. device tokenの保存
  4. send API呼び出し時に保存したdevice tokenをpayloadに含めて通知実行

一方で、実装を進める中で「Push通知時の送信先エンドポイントはどのように管理されているのか」といった点がブラックボックス的に映り、実装方針の構築に時間を要した部分もありました。 そのため通常の実装では深く意識しなくてもよいものの、背景として知っておくと挙動の理解がしやすくなるWeb Push通知の仕組みについて、整理の意味も兼ねて簡単にまとめました。

バックエンド側の実装

バックエンド側の実装設計を行うにあたり、軸にしたものはPushCrew APIとFCM APIの仕様差分でした。移行時点でTUNAGで行うWeb Push通知の利用形式として、以下3つのパターンがありました。

  • 単一トークン通知
  • 複数トークン同時通知
  • 時刻指定通知

PushCrewはこれらの通知形式をAPIとして個別に提供してくれていたため、バックエンドではトークンを用意して用途に応じたAPIを呼び出すだけで通知制御が完結していました。 しかし、FCMが提供するものは単一トークン通知用API(先のsend API)のみです。そのため、複数トークン同時通知・時刻指定通知の制御はアプリケーション側で構築する必要がありました。

また、トークンはユーザーに紐づくものですが、厳密にはユーザーのクライアント環境に応じて発行されます。同一ユーザーであっても、複数のデバイスからログインした場合はそれぞれ別IDとしてトークン発行が行われます。 そのため、アプリケーション側ではユーザー:トークン = 1:Nの対応関係を保った管理が必要となります。

このデータ構造を考慮した上で、複数トークン同時通知および時刻指定通知の制御は、Sidekiqを用いて以下のような処理フローを設計しました。

  1. イベント発火時に通知対象ユーザーを決定
  2. ユーザー単位で通知ジョブを発行
  3. 各ユーザーに紐づくトークンを取得
  4. トークン単位でFCM API呼び出しジョブを発行
%%{init: {'theme': 'dark'} }%%
sequenceDiagram

participant caller as caller
participant worker1 as UserWorker
participant worker2 as TokenWorker

caller->>worker1: perform(user_id,msg)
activate worker1
worker1->>DB: user取得
activate DB
DB->>worker1: user
deactivate DB
worker1->>DB: token取得
activate DB
DB->>worker1: token
deactivate DB

loop token_id
worker1->>worker2: perform(token_id,msg)
deactivate worker1
Note over worker2: Web Push通知実行
end

複数トークンへの同時通知については通知時に並列でジョブを走らせることで対応し、時刻指定通知においてはジョブそのものの実行時間指定で制御しています。

Web Push通知は複数の機能から利用される前提の機能であるため、通知内容と対象ユーザーを指定するだけで利用できるインターフェースに統一し、内部処理はSidekiqで吸収する方針にしました。 通知処理中の失敗に対するリトライ制御についてもアプリケーション側で個別に持たず、Sidekiqの標準機能に集約することで、通知処理全体の見通しが良くなるようにしました。

手探りの実装でしたが、他機能との連携や拡張性を見据えた構成にできたかなと思っています。

フロントエンド側の実装

バックエンド側のコアロジックが固まった後は、フロントエンド側の移行を行います。フロントエンド側で移行が必要な箇所は以下2つです。

  • トークン発行処理
  • 通知表示処理(フォアグラウンド通知、バックグラウンド通知)

まず、トークン発行処理ですが、ここでもPushCrewとFCMの仕様の違いが大きく影響しました。 FCMでのトークン発行処理はFirebase SDKが提供するgetToken()関数を用いて行う想定で、その前段としてService Workerの登録が必要となります。

Service Workerは、ブラウザに登録されるJavaScriptスクリプトで、Web Push通知機能に関してはプッシュ通知イベントが発生した際(Web Push通知を受信した際)にブラウザがワーカーとして実行し通知表示の処理を行います。

PushCrewでは、Web Push通知に必要となるService Workerが公式CDNから提供されていたため、トークン発行時にはそのエンドポイントの呼び出し、取得したスクリプトをService Workerとして登録をするだけでWeb Push通知表示が可能でした。一方で、FCMを用いたWeb Push通知では、Service Worker向けのスクリプト配信自体をサービス事業者側で担う必要があり、登録用のエンドポイントを含めた配信設計から検討する必要がありました。

このWeb Push通知処理に向けたService Worker取得用のエンドポイントを実装する上で、Web Push通知を受け取るWebサービスのドメイン直下で取得する、ということが重要になります。(例:https://service-domain/service-worker.js など)

これはFCM公式でも推奨されているプラクティスになります。

というのも、Service Workerがイベントハンドリングにおいて制御可能な範囲は、スクリプトを取得したURLのパスに依存しており、その配下にあるページやリクエストのみが対象となるからです。 例えば、Web Push通知を受けとり、そこに仕込まれたURLリンクに直接遷移する際、遷移先で特定要素にフォーカスを当てるなどの挙動が可能ですが、そのフォーカスを当てられるページは、Service Workerが取得したURLパスに依存します。

ドメイン直下で取得しない場合、先の例だと遷移先ページによってはフォーカス処理がうまく動作しない、などの通知挙動の違いが外部起因によって生まれてしまうことになります。 そのため、Web Push通知に関するService Workerの取得エンドポイントはドメイン直下で取得することが推奨されています。

TUNAGでもこのプラクティスに沿ってエンドポイントを実装しようとしたのですが、既存のアプリケーション構成との兼ね合いから、ドメイン直下でのスクリプト配布ができない状況でした。 この制約により、Service Workerの配置パスやスコープ設計を検討する必要が生じ、一部のページが制御対象外となるリスクも含めて実装判断を行うことになりました。

最終的に、先のService Workerスコープ仕様と既存の通知仕様を踏まえ、直近は問題にはならないと判断し、ドメイン直下ではなくサブパスを挟んでのService Worker登録を行う運用になりました。

通知表示処理の移行ですが、こちらは公式プラクティス通りの実装を行ったため割愛します。

トークンのライフサイクル管理

Web Push通知機能は通知先クライアントの識別用にトークンを利用しますが、これはクライアントの持ち物であり、Service Workerのライフサイクルと連動します。

これまで利用していたPushCrewでは、通知用Service Workerが専用ドメインで管理されており、トークンの有効期間についても長期利用を前提とした仕様でした。 そのため、クライアント側でトークンを定期的に再取得する必要性は高くありませんでした。

一方で、FCMではトークンは長期利用を想定したものではなく、トークンそのものが不意に利用出来なくなることを前提としたリソースとして扱っています。 そのため、今回のFCM移行において、適当なタイミングでの再取得処理を実装する必要が出てきました。

これらを踏まえ、最終的に実装した一連のトークン管理フローは以下の通りです。

%%{init: {'theme': 'dark'} }%%
stateDiagram-v2
        direction LR

    state トークン管理 {
        取得 --> 保存 : 差分あり
        取得 --> 破棄 : 差分なし
        保存 --> 取得済み
        破棄 --> 取得済み
    }
   

    [*] --> 取得可否を判定
    取得可否を判定 --> 取得 : true 
    取得可否を判定 --> 取得不可 : false
    取得済み --> 取得可否を判定 : 特定導線を踏む
    取得不可 --> [*]

TUNAGでは通知の安定性を維持するため、ユーザーアクションに紐づけてトークンの再取得を行い、トークンの鮮度を保つことにしました。 また、バックエンド側で実際に通知を行う際は通知APIのレスポンスを元に、利用期限に達したトークンについてはその時点で破棄する処理も組み込み、不要なトークンがアプリケーション上で残り続けないようにしています。

移行工程

実際の基盤移行については、TUNAGの既存ユーザーへの影響が避けられないため、以下4つのフェーズを策定して進めました。

フェーズ 通知状況
FCM移行前 PushCrew APIを用いて通知
FCM移行中 ユーザーのトークン状況に応じてPushCrew APIとFCM APIを使い分け
FCM移行後 FCM APIを用いて通知
PushCrewトークン削除 FCM APIを用いて通知

FCM移行中のフェーズにおいてはTUNAGユーザーを以下の3パターンで分類し、移行の前後で互換性を持たせることでトークン保持状況の移行期間を設けました。

ユーザーパターン 通知形式
PushCrewトークンのみ保持するユーザー PushCrew APIを用いて通知
両トークンを保持するユーザー FCM APIを用いて通知
FCMトークンのみ保持するユーザー FCM APIを用いて通知

以上を踏まえ、移行に関する全体工程は以下の通りとなりました。

通知基盤の移行フロー

PushCrew API切り離し時期の策定において、どのタイミングで行ったとしても一部の移行前ユーザーへの影響が避けられない懸念がありましたが、そこは十分な移行期間を設けることで影響の最小化を行いました。

移行期間の区切りについては、FCMトークン総数の増加を時系列でモニタリングしつつ、その傾向が安定化する時期までとしました。

その時点で移行が済んでいないユーザーについてはWeb版を積極的に利用していない層と扱いリスク許容をした上で、最終的なPushCrew APIの切り離しに至りました。

最後に

今回の記事では、TUNAGにおけるFCMへのWeb Push通知基盤移行で直面したポイントを紹介しました。 自分自身、Web Push通知の実装に本格的に関わるのは初めてで、知識のない状態から手探りで進めた部分も多かったのですが、社内のエンジニアの方々に細かく相談しつつ、最終的には無事移行することができました。

設計や実装の節目ごとに意見をもらえたことで、現在は大きな問題もなく運用できています。 今後も安定した通知体験を提供できるよう引き続き改善していく予定です。

FCMはメジャーなサービスではありますが、Web Push通知の仕組みなど前提知識が求められるケースも多く、必要最小限のAPIのみが提供されているため、移行元サービスとのギャップに戸惑う場面もあるかと思います。 この記事が同じようにFCMの導入を検討されている方々の手助けになれば嬉しいです。

スタメンでは、Rubyエンジニアに限らず全技術領域で、共にTUNAGを成長させていきたいエンジニアを募集しています。 ご興味いただけましたら、ぜひまずはカジュアル面談からご応募いただけると嬉しいです。皆さんとお会いできることを楽しみにしています。

herp.careers