JavaScriptのエラーログの収集
WebページでJavaScriptを利用する際の問題点として、利用者の環境によっては思った通りに動かないということがあげられます。
全てのブラウザとバージョンの組み合わせに対応できるJavaScriptを書くのは不可能に近いかもしれませんが、自分の構築環境だけの動作保証も問題のような気がします。
そこで第三者がJavaScriptを実行した際のエラーを受け取れるようにして、ページの改善を図りたいと思います。
エラー(例外)処理
スクリプト実行時にエラーがでて関数の途中で終わった経験は誰にでもあると思います。Google Chromeの開発者環境等はそれらのエラーを表示してくれますが、第三者のブラウザの中で表示されたものをそのまま取得することはできません。
そこで、エラー(例外)がスロー(throw)されたのを検知してそのメッセージをログ用のインターフェースに送信しないといけません。
たいていのJavaScriptのライブラリは例外がスローされるようになっていますがそれらを受け取るだけでなく、エラーを自作することもできます。その際は「throw 数値や文字列、オブジェクト」とします。
また特定のエラー用に用意されたデータ型(オブジェクト)も存在します。
エラーのキャッチ(catch)
スローされたエラーはtry {...} catch文で拾うことができます。
tryに続くブロックのどこかで例外が投げられると、catchブロックに移行します。
そのcathブロックでエラーに対するハンドリングをします。
finallyブロック
finallyブロックにはtryのブロックが正常終了するか、エラーブロックに入りそれが終了した時点、つまり正常でもエラーでも実行する処理内容を書くことができます。
tryブロックやcatchブロック内でreturnされている場合は、そのreturnは一旦保留となりfinallyブロックに移行します。finallyブロックが終了したあと保留されたreturnとなりますが、finallyブロックでreturnされていた場合はそちらの値が優先となります。
エラークラス
今回は第三者が使用する端末で発生したエラーを収集したいので、キャッチしたエラーをAJAXでログ保存のURLに転送します。
ユーザー環境の情報を転送することになりますので、オプトインを得た方が丁寧です。
ユーザー環境の取得
エラーメッセージとともに、エラーの詳細を得たり再現させるために、利用者のOSやブラウザの種類やバージョンを知りたい場合があると思いますが、これはなかなか難しい問題のようです。
以前はNavigatorからいろいろな情報を得られたのですが、プライバシーやセキュリティーの観点からそのような情報は制限しようとする潮流で、いまは軒並み廃止されたり非推奨となっています。
既存で取得できる情報としては、UserAgentがありますが、これは利用者が好きな値にセットできたりします。MDN:「ユーザーエージェント文字列を用いたブラウザーの判定」では、そもそもユーザーエージェントを利用する形でWebページの設計すること自体を考え直すように説いています。
ua-parser-js
先のような理由からブラウザのベンダーはユーザー情報の収集をすることを推奨していないようですが、そのような需要はあるらしく、それを判別するためのライブラリが存在し、週に何百万もダウンロードされているそうです。そういったライブラリはブラウザから取得できる情報を元にOSやブラウザの種類、バージョンを推定してくれます。
そのひとつにua-parser-jsがあります。
紹介しておいてなんですが、少し前にこのライブラリにはマルウェアが組み込まれてしまった経緯があるそうです。気になる方はGigazine「...人気JavaScriptライブラリが乗っ取られる...」の記事を参考にしてください。
Uaparserを用いた時に取得できる情報は主に次の通りです。こちらも主にユーザーエージェントの値を利用しているため、それを変更した開発者ツールからアクセスしたりすると、実際のものとは違った表記になります。
- ua(ユーザーエージェント)
- browser.name(ブラウザ名)
- browser.version(ブラウザバージョン)
- device.model(デバイス名)
- device.type(デバイスタイプ・mobile/tablet等)
- device.vendor(デバイス製造者)
- engine.name(エンジン名・Blink/Webkit等)
- engine.version(エンジンバージョン)
- os.name(OS名)
- os.version(OSバージョン)
- cpu.architecture(CPUアーキテクチャ・amd64等)
エラー収集スクリプト例
次のようにエラー送信クラスを定義しておきます。AJAX送信事体に環境差異によるエラーがあるといけないので筆者はライブラリは使わない方法にしましたが、逆にバージョン互換に定評のあるjQueryを使うのも方法だと思います。
sample.html
<!-- UaParserJsの読み込み -->
<script src="https://unpkg.com/ua-parser-js@1.0.2/src/ua-parser.js"></script>
<script>
let optIn = false;
const uap = UAParser();//newをしない時はそのまま各種情報のオブジェクトが返る
const ERROR_AJAX_URL="https://url";
//エラー転送関数
const sendMessage = strMsg => {
//コンソールにエラー出力
console.error(strMsg);
//情報提供に関するオプトインを得られていない場合は抜ける
//この部分の実装は別途必要です
//テストする際はコメントアウトしてください。
if (!optIn) return;
const param = 'msg='+strMsg+'&os='+uap.os.name+'&osv='+uap.os.version+'&browser='+uap.browser.name+'&browserv='+uap.browser.version;
//console.log(param);
//エラー送信
let req = new XMLHttpRequest();
req.open('POST', ERROR_AJAX_URL, true);
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8');
req.responseType = 'text';
req.send(param);
}
//tryの内側に正規の処理を書く
try {
//疑似エラーを発生させます
throw new Error('sampl.htmlでエラーが発生しました');
} catch(e) {
//エラー送信
sendMessage(e.toString());
}
</script>
このスクリプトに対して例えばPHPで受けるなら次のようにします。HTMLヘッダーについての記事でも触れたように、HTTPヘッダーの「Origin」にはスクリプトの読み込み元が入ります。それを利用して、ページへの直接アクセス等の場合はログを記録しないようにしています。
sample.php
<?php
define('LOG_PATH','./log.txt');
//HTTPヘッダーを使って経路の制限
$headers=getallheaders();
if (isset($headers['Origin'])==false || $headers['Origin']!=='https://自サイトのドメイン') {
die('経路エラー');
}
$strOs = isset($_POST['os']) ? $_POST['os'] : "";
$strOsV = isset($_POST['osv']) ? $_POST['osv'] : "";
$strBrowser = isset($_POST['browser']) ? $_POST['browser'] : "";
$strBrowserV = isset($_POST['browserv']) ? $_POST['browserv'] : "";
$strMsg = isset($_POST['msg']) ? $_POST['msg'] : "";
error_log(date('Y/m/d H:i:s').",".$strOs.",".$strOsV.",".$strBrowser.",".$strBrowserV.",".$strMsg."\r\n",3,LOG_PATH);
?>
表示されるページとログを収集するサーバーのオリジンが違うとCORSの問題が発生しますが、適切にヘッダを設定すれば回避できると思います。AJAX時のCORSについてはリンク先記事でも紹介していますのでよかったら読んでみて下さい。
追記:window.onerror
POSTD:「JavaScriptのデバッグ方法」に先のコードよりもっといい方法が紹介されていました。window.onerrorに関数を設定してAJAXを送信します。これに渡す関数には(message, url, line)の引数が設定できるということです。参考になりました。ありがとうございました。
messageにエラーメッセージ、urlにページのURL、lineに行が入ります。
window.onerror=(message, url, line, col, errorObj)=>{
console.log(message);
console.log(url);
console.log(line);
}
MDN window.onerrorによれば、line(行No.)の後に、「列(col)」と「エラーオブジェクト(errorObj)」も受け取ることができるようです。
さらに、window.addEventListenerではerrorをキーワードにすると、エラーオブジェクトを受け取る引数を持つコールバックを設定できるようになります。
window.addEventListener('error', (event) => {
log.textContent = log.textContent + `${event.type}: ${event.message}¥n`;
console.log(event)
});