こんにちは。Cacooチームの中原です。先月9月にリリースしたHTML5版 Cacoo の使い心地はいかがでしょうか?今回はそのHTML5版 Cacooで行っているクライアントサイドでの「 ロギング 」について説明したいと思います。
内容としては以下のようなものになっています。
- 報告を待つのではなく自分からJavaScriptエラーを拾う
- スタックトレース以外に必要な情報とは
- JavaScriptエラーの補足方法
- エラーをどう開発者に届けるか
攻めのJavaScriptエラー対策
ユーザのブラウザ上でJavaScriptエラーが発生した場合、どうなるでしょうか?
- 親切なユーザがうまく動作しないことをサポートへ報告してくれる
- 問題を調査して原因が判明、解決!
となれば良いですが、実際は
- エラーが起きたことに気づかない
- なにかおかしいと思ったけどリロードしたらなおった
- 動かないから使うのをやめた
- ユーザーが報告してくれたけど状況が詳しくわからず解決できない
となってしまうことが多いのではないでしょうか。
Cacooの図の編集画面では、エラーが起きるとその情報をサーバへ送信して、すぐにTypetalk(社内のチャット)に流れてくるようになっています。こうすることで大きなメリットがあります。
- ユーザーが報告しなくてもエラーが起きていることがすぐにわかり対応できる
- 問い合わせがあったときにJavaScriptエラーが原因なのか切り分けがすぐにできる
- 問い合わせの内容とエラーの内容を突き合わせることで問題の解決につながる
- いつも見ているTypetalkにエラーが流れてくるので問題を意識するようになる
実際、HTML5版Cacooの社内Beta期間に、この機能のおかげで大量のバグを発見・修正できています。また問題の報告を受ける前に開発側から「今さっき問題が起きていたようなので詳しく状況を教えてください」と聞きにいくこともありました。
スタックトレースされあれば大丈夫?!
エラーが起きたときに必要な情報ってなんでしょう? 多くの開発者が真っ先に考えるのは、エラーメッセージとスタックトレースでしょうか。実際、みなさん開発中はブラウザのコンソールを開いて、コンソール上に表示されたメッセージとスタックトレースを手がかりにデバッグを行っていると思います。
もしかしたらすでに、window.onerror
(下の方で説明します)を使ってこれらを取得し、エラーを送信している方もいるかもしれません。
しかし、Cacooの場合はこれだけでは問題の解決が難しい状況があります。
CacooではIndexedDB、XMLHttpRequestの呼び出しが多くPromiseチェーンが多用されています。そしてthen()
の中でエラーが発生しても window.onerror
は呼ばれないのです。たとえ then()
の中の try...catch
でエラーをcatchしても、非同期処理を挟んでいるのでスタックトレースだけでは処理の流れを追うことはできないのです。
では、他になにがあればよい?
操作ログ
- 図形を選択した
- 図形を動かした
- XMLHttpRequetを送信した
- 200 OKが返ってきた
- Indexed DBに情報を保存した
- 図形を削除した
例えば上記のような情報です。最新の100件分など、数に制限をつけてメモリ上に保持しています。
※ 具体的な図の内容がわかるような画像などを送信するのではなく、操作の内容や変更する値などの情報になります
操作ログは問題の原因を探る大きな手がかりになります。
例えば、不正なデータ(例えばundefined)が原因でエラーが起きたとします。ここでは不正な場合の対処でなく、不正な状態にならない根本的な対処が必要になります。操作ログからエラーが起きる前にどんな操作を行っていたか確認して、不正なデータが作られる原因を探っていきます。
時刻
当然ですが非常に重要です。エラーが発生した時刻だけでなく、上記の操作ログにも時刻を含めています。短時間で複数の操作が行われていた場合はタイミングによって問題が起きているのかもしれないですし、異常に時間がかかっている場合、特別遅い処理が存在するのかもしれません。
エラーが起きた回数
一度問題が起きると、その問題が別の問題を起こすことがあります。何度もエラーが送信されている場合、まずは最初のエラーを確認します。
他にもいくつかありますが、変わったところだとscriptタグの情報(src、type、crossorigin属性とタグの中身の先頭部分)も送っています。これはいくつかのChrome Extensionがscriptタグを挿入していて、その影響で問題が起きるということがあったからです。
エラーの捕捉
プログラムの中でチェックして異常を検出した場合、単にその詳しい状況を送信すればよいですが、想定していないエラーが起きた場合はそのエラーを捕捉する必要があります。
onerror ハンドラ (MDN web docs)
try...catch
で catchされなかったエラーをハンドルすることができます。
window.onerror = function(messageOrEvent, source, lineno, colno, error) { ... }
message:エラーメッセージ(文字列またはイベントオブジェクト)
source:エラーが発生したスクリプトのURL(文字列)
lineno:エラーが発生した行番号(数値)
colno:エラーが発生した行の列番号(数値)
error:エラーオブジェクト(オブジェクト)
See the Pen window.onerror by shoji (@ShojiNakahara) on CodePen.
ブラウザのサポート状況
IE10以前では利用できません。
unhandledrejection, rejectionhandledイベント (MDN web docs)
これはリジェクトされた Promise が then()
、catch()
でハンドリングされていないときに発生するイベントです。
前にも述べたとおり、CacooではIndexedDBやXMLHttpRequestなど非同期処理が多用されPromiseチェーンがたくさん出てきます。Promiseチェーンの中(then()
の中)でエラーが起きた場合、window.onerror
ではハンドリングされずに rejected なPromiseとして処理が続きます。したがって、Promiseを利用している場合はこちらも利用します。
window.addEventListener("unhandledrejection", function(event){ ... });
window.addEventListener("rejectionhandled", function (event) { ... });
2つイベントがあるのは、Promiseがrejectされたあと遅れてcatchされた場合に対応するためです。下のサンプルコードを見ていただけるとわかると思います。
したがって未catchのリジェクトを正しく取得するためには setTimeout()
などで一定時間catchされてないか確認する必要があります。
See the Pen unhandledrejection by shoji (@ShojiNakahara) on CodePen.
ブラウザのサポート状況 (Can I use)
2017/10現在、ChromeとSafari 11 で利用できます。未対応のブラウザが多い状態ですが、エラーの捕捉であれば使える環境だけでも積極的に使っていくべきだと思います。
クロスドメインでの”Script error.”
上記の window.onerror
でエラーを取得しても ”Script error.” とだけしか情報がない場合があります。JavaScriptファイルをCDNなど別ドメインで配布している場合に発生します。セキュリティ上、情報が取れないようになっているんですね(W3C)。
この場合は crossorigin=”anonymous”
という属性をscriptタグに付けて、JavaScriptファイルのHTTPレスポンスにもCORSヘッダを追加してください。これで詳細な情報が取れるようになります。
scriptタグの例
<script type="text/JavaScript" src="abc.js" crossorigin="anonymous"></script>
HTTPレスポンスヘッダの例
access-control-allow-credentials: true
access-control-allow-methods: GET
access-control-allow-origin: https://cacoo.com
エラーを開発者へ迅速に届ける
最初の方で述べた通り、Cacooではエラーが起きるとすぐに社内のTypetalkに流れるようになっています。
Typetalk上でエラーメッセージなどの概要を見て、Kibanaから操作ログ、スタックトレース、HTTPヘッダなどの詳細な情報を確認します。KibanaからはフロントのNginxのログも見れるので、必要に応じてそれ以前HTTPリクエストも確認していきます。
まとめ
Cacooのクライアントサイドで行っているエラー発生時の対応を紹介しました。上で述べた通り、HTML5版 Cacooの社内Beta期間にエラーはかなり減らしたのですが、リリースした直後は大量のエラーが送信されてきました(数秒で1件!)。社内Beta期間には見ることのなかったエラーばかりでした。公開直後から調査・対応を進めて今はかなり減ってきています。
もちろん、エラーの内容や操作ログを見ても原因がわからなかったり再現できないことはたくさんあります。また、正しく動作していないけどエラーは起きていないという状況には対応できません。
ただ解決できる問題もたくさんあるのと、エラーが把握できるということで非常に役に立っています。
みなさんも、もしJavaScriptエラーで悩んでいたら同じような仕組みを検討してみてはどうでしょうか?