Patrick Crosby's Internet Presents

JavaScript Error Logging Service using Go and jsErrLog

28 Mar 2014

In a "standard" web application where nearly all the work is done on the server, it's fairly straightforward to track errors. They happen on your servers, you can log them, you can send emails when they happen, whatever you want.

But with web apps where more and more of the functionality is in client-side JavaScript, the errors on the server disappear. The app developers have no idea they are happening unless users notify them.

It's no surprise that there are a lot of services to help with this problem. Paul Irish compiled a list of some of them. Some only do JavaScript errors, some track all kinds of errors, some are based on window.onerror, some on catching exceptions.

I wanted something simple to start. I just want to know that an error occurred. While a stack trace would be nice, right now I'm ok without it. So I'm trying out jsErrLog. It binds a function to window.onerror and sends any error information to a web service. There is a free service you can use that's running on AppEngine, but I chose to send the data to our servers. All the window.onerror solutions can only send the URL, file, line number, error message, and some newer browsers include the column number.

I wrote a small web service to receive the data from the jsErrLog script. I had a few requirements for it: send errors periodically via email, don't store them anywhere else, don't send duplicates, don't use too much memory.

Here's the structure that holds the data from jsErrLog:

type entry struct {
        url      string
        filename string
        line     string
        col      string
        errmsg   string
}

There's a map that uses the entry struct as a key. You could change the bool to an int if you care about how many times an error occurred. I'm using a sync.RWMutex to protect the map.

var dedupe map[entry]bool
var lock sync.RWMutex

The init function creates the initial map and starts the dump goroutine.

func init() {
        dedupe = make(map[entry]bool)
        go dump()
}

The request handler function:

func ErrLog(w http.ResponseWriter, r *http.Request, s *bingo.Session) {

It starts by getting the form values into an entry:

        var e entry
        id := r.FormValue("i")
        e.url = r.FormValue("sn")
        e.filename = r.FormValue("fl")
        e.line = r.FormValue("ln")
        e.col = r.FormValue("cn")
        e.errmsg = r.FormValue("err")

Then it checks to see if it already has this error. It read-locks the mutex before the check.

        lock.RLock()
        _, exists := dedupe[e]
        lock.RUnlock()

If it doesn't exist, it write-locks the mutex and puts the error info in the map. It checks to make sure there aren't too many elements already in the map as a safeguard.

        if !exists {
                lock.Lock()
                if len(dedupe) < 1000 {
                    dedupe[e] = true
                }
                lock.Unlock()
        }

The jsErrLog script wants JavaScript returned to it, including a special call to remove a script that it inserts.

        w.Header().Set("Content-Type", "text/javascript")
        fmt.Fprintf(w, "jsErrLog.removeScript(%s);", id)
}

Finally, the dump function just loops forever. It sleeps for five minutes, then checks to see if there's anything in the dedupe map. If there is, it emails it to a system account and clears the map.

func dump() {
        for {
                time.Sleep(5 * time.Minute)
                lock.Lock()
                if len(dedupe) > 0 {
                        sendmail()
                        dedupe = make(map[entry]bool)
                }
                lock.Unlock()
        }
}

It's pretty simple, but it does everything we want. If we need stack traces, we'll give Airbrake a try.