グローバルサービスでのタイムゾーンとの向き合い方 -ケーススタディ編-

前回の記事に引き続き、Web developer の大庭 (@ohbarye) です。

先日『グローバルサービスでのタイムゾーンとの向き合い方』にてタイムゾーンにまつわる諸問題や解決策を1つの記事にまとめました。

しかし同記事はテクニック集としての側面が多く、Quipper の開発者として実際にどのような問題に直面するのかいまいち伝わりづらかったかもしれません。

そこで今回は Quipper の機能に関連するタイムゾーンを考慮した設計と実装の一部をケーススタディ編と題し、改めてご紹介します。前回の記事と一部重複する内容はありますが具体例を交えた復習として参考になれば幸いです。

目次

  • 1 宿題管理機能
    • モデルイメージ
    • 設計に関する問いかけ
    • 解答・解説
  • 2 TODO 管理機能
  • 3 キャンペーン機能
  • おわりに

1. 宿題管理機能

Quipper School およびスタディサプリの宿題管理機能は日々の業務で忙しい先生を助けるコア機能の1つです。

モデルイメージ

まず、開始日と終了日を持った Assignment (宿題) クラスを用意します。

class CreateAssignments < ActiveRecord::Migration
  def change
    create_table :assignments do |t|
      t.datetime :starts_at
      t.datetime :ends_at
    end
  end
end

設計に関する問いかけ

この宿題の開始日と終了日を以下のような UI で設定する場合、アプリケーションは入力された日時をどのタイムゾーンの値として受け取るべきでしょうか?

  1. 登録する先生のクライアント (マシン or ブラウザ) のタイムゾーン
  2. DB / Rails のデフォルトタイムゾーン (共通のタイムゾーンとする)
  3. 登録する先生の所属するクラス or 学校のタイムゾーン

f:id:ohbarye:20161207152921p:plain

解答・解説

ここで入力している先生が日本の先生だと仮定すると、終了日の 2016/11/11 は「2016/11/11 23:59:59 JST までに宿題を提出してほしい」と意図しているはずです。

1では先生が出張先や休暇旅行先の異なるタイムゾーンで宿題を作ろうとすると意図しない結果になります。例えばロンドンから登録した時に 2016/11/11 23:59:59 UTC として扱ってしまうと、これは 2016/11/12 08:59:59 JST になりますから、宿題の期限が約9時間も長く設定されてしまいます。

2も同じ理由で NG です。先生が意図するタイムゾーンとデフォルトタイムゾーンがたまたま一致しない限りずれが生じます。

というわけで Quipper では3を採用し、学校のタイムゾーンを信頼しています。*1永続化の処理は以下のようになります。

def create_assignment!
  Time.use_zone(current_user.organization.timezone) do
    starts_at = Time.zone.parse(start_date).beginning_of_day
    ends_at   = Time.zone.parse(end_date).end_of_day

    Assignment.create!(starts_at: starts_at, ends_at: ends_at)
  end
end

ちなみに、生徒が提出する際に期限切れかどうかを判定する場合はどうなるでしょうか。

class Assignment
  def expired?
    Time.zone.now > self.ends_at
  end
end

Rails アプリケーションと DB それぞれのデフォルトタイムゾーンを統一している場合、両辺の値は同じタイムゾーンでの比較になるのでこれだけで OK です。「締め切り2日前」のような条件であれば Time.zone.now > self.ends_at - 2.day となります。

こうしたタイムゾーンの扱いを誤り、クライアント側のタイムゾーンや値を信頼するような実装をしてしまうと「提出期限切れなのに提出可能になる」ような"チート行為"が出来てしまいます。これは教育アプリケーションとしては致命的で、先生からの信頼を失うことになります。

2. TODO 管理機能

次に、生徒が自分で学習する日付と講座を設定できる TODO 管理機能を見てみましょう。

画面仕様

以下の画面には 2016/11/05 00:00:00 < now < 2016/11/05 23:59:59 であれば 2016/11/05 に「やるぞ!」と設定した講座が表示されなければいけません。

f:id:ohbarye:20161207152940p:plain

信頼すべきタイムゾーン

この時に使うべきタイムゾーンはユーザーによって異なります。これは色んな考え方があると思いますが、現在採用している例だとユーザーが持つサブスクリプションに紐付くタイムゾーンで判定をしています。

クライアントからリクエストヘッダーにタイムゾーン情報を詰めて送らせるという案もかつてあったのですが、ブラウザ間の差異に悩まされること、デバッグのし辛さ、チートの可能性が増すといった理由から、極力サーバサイドで判定できるよう設計すべき、という結論になりました。

3. キャンペーン機能

最後に、特定の期間だけバナー広告を表示するキャンペーン機能について。 この機能ではタイムゾーンを入力するインターフェースの問題を扱っています。

画面仕様

キャンペーンの登録は非エンジニアでも行えるよう社内向けアプリケーション内に実装されています。期間や対象などの条件を入力し、 Campaign モデルを作成する処理です。

画面の入力インターフェースは以下のようになっています。

f:id:ohbarye:20161207153007p:plain

タイムゾーンをユーザーに入力してもらう

先生向けのアプリケーションとの最大の違いは、タイムゾーンをユーザー (操作者) に入力させている点です。

一般の先生・生徒に対しての「タイムゾーンを意識して日時を入力してください」という要請はあまりに酷な UI / UX ですが、 Quipper の各国スタッフにとってみれば作成したいキャンペーンがどの国 (タイムゾーン) のユーザー向けなのかは自明なのでまず悩むことなく登録できます。

こうした画面を用意すれば各国ごとにカスタマイズしたアプリケーションを作る必要はありません。

余談ですが、Quipper のプロダクト開発では「この機能は別のプロダクト、別の国で使えないか?」「別の国から見た時にどう見えるか?」といったグローバルな視座が非常に重要です。そしてこの視点から物事を考えるときにもやはりタイムゾーンの考慮が求められます。

リテラシーへの配慮

キャンペーン管理機能にはやや特殊な例でしたが、利用者のリテラシーを考えた日時・タイムゾーンの入力も考慮することも大事です。

もしリテラシーがそれほど高くないユーザー向けに同じことを聞きたいシーンがあるとしたら、質問を「お住まいはどちらですか?」などにしてプルダウンで選択させるのがよいでしょう。

ちなみに rake time:zones:all で有効なタイムゾーンを確認できるので選択肢はこの中から選ばせるのが望ましいと思います。

おわりに

2回に渡ってタイムゾーンに関する記事を書きましたがいかがだったでしょうか。

日時やタイムゾーン周りの話は地味ではありますがどのアプリケーションにとってもクリティカルなところだと思います。(たとえば課金処理は、1つ間違えればプロダクトへの信用を失うどころか社会的責任を求められるかもしれない減点方式の世界です)

こうした「地味だけどやらなきゃいけない」問題はどの会社も成長拡大する中でいずれ直面するものなので、そこで折れないために何が必要かを多くの人が考えているはずです。

この問に対する答えの一つが「派手さはなくとも当たり前のことを当たり前にやる」という、Quipper がかねてより大事にしてきているモットーだと個人的には思っています。


★Quipper日本オフィスでは仲間を募集しています。是非お気軽にご応募ください。★

*1:複数のタイムゾーンにまたがる学校というのは現時点では考慮しませんが、分校という形で2つの学校としてデータ上は表現できるでしょう。