一部の端末だけが固まった大更新に潜む罠
最新へ大きく上げたダイアログライブラリが、一部の端末でだけログイン画面を覆って固まった。手元では再現しない不具合の核心を、現場のDOMが指し示す。原因特定より「依存の排除」で直した後日談。
続き物の話:橋は架けた。だが
少し前に、ダイアログ表示ライブラリ SweetAlert2 を非常に古い v3系(3.3.6)から最新の v11系(11.26.25)へ一気に上げた話を書いた。約337箇所の旧API呼び出しを書き換えず、ライブラリ末尾に薄い互換シムを1枚足して橋渡しした、という顛末だ。橋はきれいに架かった。手元でもテスト環境でも、ダイアログは正しく開き、正しく閉じた。
しばらくして、現場から報告が届いた。「一部のパソコンで、ログイン画面のボタンが押せない」。
これが今日の後日談だ。バージョンを上げすぎた代償は、私たちの手元ではなく現場の一部の端末に出た。
いちばん厄介な種類の不具合
症状はこうだった。ログイン画面が透明な何かに覆われ、クリックがまったく効かない。マウス操作は一切通らないのに、Tab キーでの移動だけは効く。そして全部の端末では起きない。 同じ画面、同じ操作なのに、ある端末では固まり、別の端末ではまったく問題ない。ユーザーも問わない。その端末を使う人は誰であっても固まる。
これはデバッグにおいて最も厄介な形だ。手元で再現できない。「たまに」でもなく「特定の環境だけ」で、しかもその環境の何が引き金かがわからない。ログを見ても、コードを追っても、自分の PC では一度も再現しない。確かめようがない。
正直に言えば、私たちは世の中のすべての端末を理解してはいない。 ブラウザのバージョン、GPU、描画設定、リモートデスクトップやシンクライアントのような画面転送環境。組み合わせは無数にある。作った側が把握しきれない差異の海の中で、あるひとつの組み合わせだけが地雷を踏んでいた。
解決の糸口は、ユーザーが見せてくれた
膠着を破ったのは、こちらの推理ではなかった。
固まった端末の利用者が、ブラウザの開発者ツールを開いて、その画面のDOM構造そのものを見せてくれたのである。
そこには、中身が空っぽのオーバーレイ要素が一枚、画面全体にかぶさっていた。画面いっぱいに固定表示され、最前面に居座り、あらゆるクリックを吸い込んでいる。ダイアログの「箱」だけが、閉じられずに取り残されていた。手でその要素を削除すると一時的にクリックが戻る、という挙動まで教えてくれた。
これで一気に像を結んだ。ダイアログは閉じたはずなのに、入れ物だけが DOM に残り続けている。中身は消えて見えないから、利用者には透明な壁に映る。原因は閉じる処理そのものにあった。
現場の利用者が見せてくれた一枚の DOM が、こちらの環境では絶対に再現しなかった不具合の核心を、まっすぐ指し示していた。作り手が全端末を持てない以上、答えは現場が握っている。
原因:閉じるアニメの「完了通知」を待つ設計
新しい版の閉じる処理は二段構えだった。まずダイアログにフェードアウトのアニメーションを掛け、そのアニメーションの完了イベントが発火して初めて、入れ物を DOM から取り除く。
平時はこれで問題ない。0.15秒のフェードが走り、終わった合図とともに箱が消える。だが落とし穴がある。ライブラリは「アニメを待つべきか」を、アニメーションの時間が設定されているかどうかだけで判断していた。つまり時間は予約されているのに、その端末では実際にはアニメが完走せず、完了イベントが飛んでこない。 そういう環境だと、箱を消す処理が永遠に呼ばれない。
アニメの開始予約はあるのに、完了通知だけが来ない。描画合成が簡略化された環境や、イベントの取りこぼしが起きる環境では、これが現実に起きる。正常な端末では0.15秒できれいに消え、そうでない一部の端末でだけ、空の箱が画面に貼り付いたまま残った。
大きく上げたことの意味が、ここで効いてくる。互換シムは API の呼び方——引数や戻り値——の橋は架けた。だがライブラリ内部の実装の変化まではシムの守備範囲ではない。 旧版と新版とで閉じ方の作りが変わっていて、その新しい作りが特定の環境依存を新たに持ち込んでいた。何世代も飛び越えて上げると、こうした見えない挙動の変化を一緒に連れてくる。
直し方:原因の端末を追うより、依存そのものを断つ
ここで方針を決めた。「完了イベントが来ない端末」を突き止めて回るのは費用対効果が悪い。要因は環境依存で、再現も不安定。追いかけるほど泥沼になる。
ならば、イベントに依存する処理そのものを消す。
やったことは二つ。ひとつは閉じるアニメーションを無効にすること。アニメの時間がゼロになれば、ライブラリは待つ判断をやめ、その場で同期的に箱を取り除く。完了イベントを待つ経路を、根元から通らなくする。見た目のフェードは失われるが、確実さと引き換えなら安い。
もうひとつは保険だ。万一それでも箱が残った場合に備え、閉じ処理を検知して、取り残された入れ物を少し後に強制的に片づける自己修復を足した。正常な端末では本体が先に片づけ済みなので、この保険は空振りする。無害なまま、いざというときだけ働く。
再現できない不具合に対しては、「原因を特定して直す」より「原因を問わず成立させる」ほうが、ときに正しい。狙ったのは前者の華々しさではなく、後者の確実さだった。
届け方:ユーザーが自分でZIPを当てられる仕組み
直したコードを、どうやって全国の現場へ届けるのか。ここで効いたのが NGW UPDATE の仕組みだ。
このシステムは、更新を一式の ZIP にまとめて配る。利用者は管理画面からアップデート ZIP をダウンロードし、上書き適用するだけで最新になる。CMS も大掛かりな配信基盤も要らない。今回の修正もこの経路で複数の団体へ配布した。
修正の実体は差し替えるファイルだけで完結するよう作ってある。ライブラリ本体と、古いキャッシュを一度だけ捨てさせるためのバージョン付け。この二つを ZIP に同梱すれば、当てた端末は次の読み込みで確実に新しい版を掴む。配布のあと、固まっていた当の端末で「無事にログインできた」と報告が返ってきて、ようやく一件落着となった。
作り手が現場の全端末を持てないなら、せめて現場が自分の手で最新を当てられるようにしておく。この仕組みが、再現できない不具合の修正を実地の復旧確認まで最短で運んでくれた。
AIとの協働作業での学び
ミドルウェアの大更新には危険が潜んでいる。教訓を三つに畳んでおく。
- 何世代も飛び越える更新は、APIだけでなく内部の挙動も変える。 互換シムは API の橋を架けられても、実装の変化——今回で言えば閉じ方に潜んだ環境依存——までは吸収しない。上げ幅が大きいほど、見えない挙動変化を一緒に連れてくると身構えるべきだ。
- 作り手は全端末を理解できない。だから現場の観察が最強の情報源になる。 こちらで再現できない不具合の核心を指したのは、利用者が見せてくれた一枚の DOM だった。ユーザー本意で作ることは、こういう時に情報という形で返ってくる。
- 再現できないものは、原因を追うより依存を断つ。 イベント発火のような環境依存に賭けた処理は、環境が変われば裏切る。特定の端末を追い続けるより、その依存そのものを設計から消すほうが確実で速いことがある。
バージョンを上げすぎたのは反省すべき点だ。だが上げすぎた後にどう畳むかも、同じくらい設計の腕が問われる。今回は「依存を断つ」と「現場が自分で当てられる」の二枚で畳んだ。
……それにしても、開発者ツールを開いて DOM を見せてくれた現場の方には、いまだに頭が上がらない。こちらが何日にらんでも出てこなかった答えを、向こうは画面をひとつ開いて差し出してきたのである。作り手というのは、案外いちばん遠いところに立っている。