You Really Should Log Client-Side Errors

Apr 04, 2012

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) {
    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

def extract_keys(hash, *keys)
  hash.reject{|k| !keys.include?(k)}

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)

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.