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.