🕰️

Timezone起因のバグを修正してDevtoolで動作確認する

2025/01/27に公開

はじめに

  • 前提:
    • Laravel + React + AWS Chime Meeting
    • 特定の時間になったら、ブラウザベースで通話に参加できる(ブラウザ用zoomっぽい感じのアプリ)
  • サーバサイドで日時を日本時間(JST)のまま返却したところ、シンガポールなど別のタイムゾーンの利用者がブラウザ上で正しい開始時刻を認識できなくなった
  • このインシデントの発生から対応策、さらにDevToolsを使ったタイムゾーン検証の手法まで解説する

インシデント概要

時差が原因で通話開始タイミングにズレ

あるユーザー(シンガポール在住)が、「13:00開始」のビデオ通話に参加しようとしたところ、画面上では「開始まであと1時間+」と表示されてしまい、実際はもう開始時刻を過ぎているのに参加が許可されないという事象が起きた

事象の具体例

  • サーバがLaravelベースで、DBに"2025-01-27 13:00:00"のようなJST日時を保存
  • フロントのReactがこの値をそのままnew Date("2025-01-27 13:00:00")で扱う
  • ユーザーがシンガポール時間(UTC+8)でChromeを利用した場合、ブラウザは文字列を「現地時間」としてパースできず、実質的に2025-01-27T13:00:00Zとは解釈されない(パースに失敗・または意図しない解釈)
  • 結果、意図しないタイミングで「通話まであとXX時間」と計算される
    • シンガポールとの時差は1時間早いので、日本が13:00ならシンガポールは12:00になる

背景と問題の原因

JSTのまま日時を返すことの問題

多くのプロジェクトでは「サーバサイドはJST運用」というケースが珍しくない。MySQLにもtimestampdatetimeでJST基準のままレコードが保存されている。しかし、REST APIを海外のユーザーやグローバルに提供する場合、日時をそのまま文字列で返すとブラウザなどクライアント環境がどのように解釈するかを制御しづらい

  • "2025-01-27 13:00:00" という文字列はブラウザ上でISO8601("2025-01-27T13:00:00Z")とはみなされず、タイムゾーンの明示がない
  • その結果、ローカル環境によって解釈が変わる/パースエラーが起きる
  • 当然、時差のある地域では実際の時間とズレたカウントダウンが表示される

LaravelでのTimeZone設定

Laravelのconfig/app.phpには'timezone' => 'Asia/Tokyo'などと書くことでアプリ全体でJSTを標準とできるが、APIのレスポンスに生の時刻文字列を返すのは要注意
グローバル対応の場合はよくDBやAPIの出力はUTCに統一し、クライアント側でユーザーのタイムゾーンに合わせるという設計を行う。

修正対応の方針

1. UTC基準で日時を返却する

一番シンプルな対処法としては「サーバサイドでCarbonなどを使い、UTCに変換したISO8601形式の日時を返却する」こと。
例えば以下のような形が望ましい。

2025-01-27T04:00:00.000000Z

こう返すことで、受け取ったクライアント側はnew Date("2025-01-27T04:00:00.000000Z")とすればUTCとして正しくパースできる。

2. クライアント側でローカルタイムへの変換

Reactなどフロント側では、受け取ったUTC時刻をnew Date(UTCの文字列)でJSのDateオブジェクトにし、その後ユーザーのローカルタイムに変換。たとえばカウントダウンを表示する際には以下のような処理が基本になる。

const startUtcString = "2025-01-27T04:00:00.000000Z";
const startDateLocal = new Date(startUtcString); // これで自動的にローカル時間のDateオブジェクト

// カウントダウン用の差分(ミリ秒)を計算
const nowLocal = new Date();
const diff = startDateLocal.getTime() - nowLocal.getTime();

どの場所からアクセスしても、ユーザー端末のローカル時刻に合わせて適切な差分が計算される。

具体的な実装例

1. LaravelのResourceでUTC変換

LaravelResourceクラスやtoArray()の中などで、DBに保存されているdatetime(JST)を返却時にUTCへ変換する。Carbonを使うなら以下のように書ける。

public function toArray($request)
{
    return [
        'id' => $this->id,
        // 他のフィールド...
        // そのまま日付フィールドを返せば、UTC形式で返してくる
        'start_at' => $this->start_at,
        'end_at' => $this->end_at,
        // ...
    ];
}

DBに"2025-01-27 13:00:00"(JST)で格納されていても、**レスポンス時にはUTCの"2025-01-27T04:00:00.000000Z"**のような文字列となる。

2. React側でローカル時刻に変換して表示

foo.start_atはサーバレスポンスで返ってくるUTCのISO8601文字列。
JSでnew Date("2025-01-27T04:00:00Z").getTime()すれば、そのままローカルタイムゾーン内のDateオブジェクトとして扱える。

const timestamp = new Date(foo.start_at).getTime()

DevToolsによるタイムゾーン検証

タイムゾーンの検証はChromeブラウザのDevToolsを使えば簡単。

  1. Chrome DevToolsを開く
    ブラウザで右クリック→「検証」またはF12でDevToolsを開く。

  2. Sensorsを表示する
    DevTools内で右上のメニュー(…)→「More tools」→「Sensors」と進む。

  3. Time zone override
    LocationTime zone overrideといった欄が出るので、Time zone overrideをAsia/Singaporeに設定。

  4. ページをリロード
    この状態で該当ページを再読み込みすれば、ブラウザがシンガポール時間として日時を扱う。するとカウントダウンや日付表示をシンガポール時間基準でシミュレートできる。


サンプル動作結果

実際の表示例

シンガポール時間(UTC+8)での表示

  • Laravelが返す時刻: 2025-01-27T04:00:00.000000Z (UTC)
  • シンガポール(UTC+8)では同日の12:00ローカル時刻
  • もし記事閲覧時が11:30(UTC+8)なら「開始まであと30分」のような表示がされる

日本時間(UTC+9)での表示

  • 同じ時刻を日本時間で見ると13:00
  • つまり日本時間で11:30にアクセスすると「開始まであと1時間30分」とカウントされる

このように、サーバサイドの日時管理がUTCになっていれば、クライアントが自動的にローカル時刻として解釈するため、ユーザーごとに正しい時刻表現が得られる。


まとめ

  • 今回のインシデント原因:
    • 海外ユーザーの存在と時差を考慮せず、JSTで保存された日時をそのまま文字列として返却していた
  • 対応策:
    1. APIで返す日時をUTC(ISO8601)に統一
    2. クライアント側でnew Date()してローカルタイムに変換
    3. 必要に応じて、カウントダウンや時刻表示を適切にローカルタイムへ変換

Chrome DevToolsのTime zone override機能を使えば、本番環境さながらのローカル時間を擬似的にテストできるので、海外ユーザーが利用するWebアプリのデバッグには便利。

Discussion