You Really Should Log Client-Side Errors
Let's keep this short. Too few websites log JavaScript errors. Let's build a simple system to track client-side errors.
First, we'll create a logError
method in JavaScript:
function logError(details) {
$.ajax({
type: 'POST',
url: 'http://mydomain.com/api/1/errors',
data: JSON.stringify({context: navigator.userAgent, details: details}),
contentType: 'application/json; charset=utf-8'
});
}
This assumes jQuery is available, and if it isn't, you can change the ajax implementation.
How do we use this function? At the very least, we want to hook into window.onerror
:
window.onerror = function(message, file, line) {
logError(file + ':' + line + '\n\n' + message);
};
If you are using jQuery, then hooking into ajaxError
isn't a bad idea either (as pointed out in a HN comment, this probably is a bad idea, since ajaxError
fires for server errors which server-based logging is a much better fit):
$(document).ajaxError(function(e, xhr, settings) {
logError(settings.url + ':' + xhr.status + '\n\n' + xhr.responseText);
});
With that bit of client-side code in place, we can write a little server. Here's my complete sinatra app to handle this:
require 'sinatra'
require 'JSON'
require 'redis'
configure { set :redis, Redis.new }
post '/api/1/errors' do
id = save_error(extract_keys(JSON.parse(request.body.read), 'context', 'details'))
content_type :json
{:id => id}.to_json
end
private
def extract_keys(hash, *keys)
hash.reject{|k| !keys.include?(k)}
end
def save_error(error)
id = Digest::MD5.hexdigest(error['details'])
error['time'] = Time.now.utc
settings.redis.zadd('errors', error['time'].to_i, id)
settings.redis.lpush(id, error)
id
end
This implementation is using Redis, but you could store the data in anything. The way that it works is by calculating the md5 value of error's details
and store that md5 value in a sorted set ranked by time. We then store the error details into a list for that particular hash.
The above code is only focused on storing the errors. If you want to build a web-based front-end to display them, or maybe even a socket.io page to display errors in real time, you can. You might even just write a script that runs every once and a while and generates an email report.
Without looking at a specific display implementation, let's look at how we might get our data back out of Redis.
First, to get the last X errors, we can use zrevrange errors 0 X
. We want the reverse range because new errors have a higher rank (Time.now.utc.to_i
). If you wanted errors that happened within a specific time frame, you could use:
to = Time.now.utc
from = to - 86400 # (3600 * 24, 1 day)
redis.zrevrangebyscore 'errors', from, to
This returns all the hashes. We can then use lindex HASH 0
on each hash to get the last instance of that particular error. We can also use llen HASH
to get how often a specific error has happened
That's pretty much it. It's as simple as it is useful.