diff --git a/public/javascripts/notifier.js b/public/javascripts/notifier.js
new file mode 100644
index 0000000..29ee3de
--- /dev/null
+++ b/public/javascripts/notifier.js
@@ -0,0 +1,293 @@
+var Hoptoad = {
+ VERSION : '2.0',
+ NOTICE_XML : '\
+ \
+ \
+ \
+ hoptoad_notifier_js\
+ 2.0\
+ http://hoptoadapp.com\
+ \
+ \
+ EXCEPTION_CLASS\
+ EXCEPTION_MESSAGE\
+ BACKTRACE_LINES\
+ \
+ \
+ REQUEST_URL\
+ REQUEST_COMPONENT\
+ REQUEST_ACTION\
+ \
+ \
+ PROJECT_ROOT\
+ production\
+ \
+ ',
+ ROOT : window.location.protocol + '//' + window.location.host,
+ BACKTRACE_MATCHER : /^(.*)\@(.*)\:(\d+)$/,
+ backtrace_filters : [/notifier\.js/],
+
+ notify: function(error) {
+ var xml = escape(Hoptoad.generateXML(error));
+ var host = Hoptoad.host || 'hoptoadapp.com';
+ var url = '//' + host + '/notifier_api/v2/notices?data=' + xml;
+ var request = document.createElement('iframe');
+
+ request.style.width = '1px';
+ request.style.height = '1px';
+ request.style.display = 'none';
+ request.src = url;
+
+ document.getElementsByTagName('head')[0].appendChild(request);
+ },
+
+ setEnvironment: function(value) {
+ var matcher = /.*<\/environment-name>/;
+
+ Hoptoad.NOTICE_XML = Hoptoad.NOTICE_XML.replace(matcher,
+ '' +
+ value +
+ '')
+ },
+
+ setHost: function(value) {
+ Hoptoad.host = value;
+ },
+
+ setKey: function(value) {
+ var matcher = /.*<\/api-key>/;
+
+ Hoptoad.NOTICE_XML = Hoptoad.NOTICE_XML.replace(matcher,
+ '' +
+ value +
+ '');
+ },
+
+ setErrorDefaults: function(value) {
+ Hoptoad.errorDefaults = value;
+ },
+
+ generateXML: function(errorWithoutDefaults) {
+ var error = Hoptoad.mergeDefault(Hoptoad.errorDefaults, errorWithoutDefaults);
+
+ var xml = Hoptoad.NOTICE_XML;
+ var url = Hoptoad.escapeText(error.url || '');
+ var component = Hoptoad.escapeText(error.component || '');
+ var action = Hoptoad.escapeText(error.action || '');
+ var type = Hoptoad.escapeText(error.type || 'Error');
+ var message = Hoptoad.escapeText(error.message || 'Unknown error.');
+ var backtrace = Hoptoad.generateBacktrace(error);
+
+
+ if (Hoptoad.trim(url) == '' && Hoptoad.trim(component) == '') {
+ xml = xml.replace(/.*<\/request>/, '');
+ } else {
+ var data = '';
+
+ var cgi_data = error['cgi-data'] || {};
+ cgi_data["HTTP_USER_AGENT"] = navigator.userAgent;
+ data += '';
+ data += Hoptoad.generateVariables(cgi_data);
+ data += '';
+
+ var methods = ['params', 'session'];
+
+ for (var i = 0; i < 2; i++) {
+ var type = methods[i];
+
+ if (error[type]) {
+ data += '<' + type + '>';
+ data += Hoptoad.generateVariables(error[type]);
+ data += '' + type + '>';
+ }
+ }
+
+ xml = xml.replace('', data + '')
+ .replace('REQUEST_URL', url)
+ .replace('REQUEST_ACTION', action)
+ .replace('REQUEST_COMPONENT', component);
+ }
+
+ return xml.replace('PROJECT_ROOT', Hoptoad.ROOT)
+ .replace('EXCEPTION_CLASS', type)
+ .replace('EXCEPTION_MESSAGE', message)
+ .replace('BACKTRACE_LINES', backtrace.join(''));
+ },
+
+ generateBacktrace: function(error) {
+ error = error || {};
+
+ if (typeof error.stack != 'string') {
+ try {
+ (0)();
+ } catch(e) {
+ error.stack = e.stack;
+ }
+ }
+
+ var backtrace = [];
+ var stacktrace = Hoptoad.getStackTrace(error);
+
+ for (var i = 0, l = stacktrace.length; i < l; i++) {
+ var line = stacktrace[i];
+ var matches = line.match(Hoptoad.BACKTRACE_MATCHER);
+
+ if (matches && Hoptoad.validBacktraceLine(line)) {
+ var file = matches[2].replace(Hoptoad.ROOT, '[PROJECT_ROOT]');
+
+ if (i == 0) {
+ if (matches[2].match(document.location.href)) {
+ backtrace.push('');
+ }
+ }
+
+ backtrace.push('');
+ }
+ }
+
+ return backtrace;
+ },
+
+ getStackTrace: function(error) {
+ var stacktrace = printStackTrace({ e : error, guess : false });
+
+ for (var i = 0, l = stacktrace.length; i < l; i++) {
+ if (stacktrace[i].match(/\:\d+$/)) {
+ continue;
+ }
+
+ if (stacktrace[i].indexOf('@') == -1) {
+ stacktrace[i] += '@unsupported.js';
+ }
+
+ stacktrace[i] += ':0';
+ }
+
+ return stacktrace;
+ },
+
+ validBacktraceLine: function(line) {
+ for (var i = 0; i < Hoptoad.backtrace_filters.length; i++) {
+ if (line.match(Hoptoad.backtrace_filters[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ generateVariables: function(parameters) {
+ var key;
+ var result = '';
+
+ for (key in parameters) {
+ result += '' +
+ Hoptoad.escapeText(parameters[key]) +
+ '';
+ }
+
+ return result;
+ },
+
+ escapeText: function(text) {
+ return text.replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/'/g, ''')
+ .replace(/"/g, '"');
+ },
+
+ trim: function(text) {
+ return text.toString().replace(/^\s+/, '').replace(/\s+$/, '');
+ },
+
+ mergeDefault: function(defaults, hash) {
+ var cloned = {};
+ var key;
+
+ for (key in hash) {
+ cloned[key] = hash[key];
+ }
+
+ for (key in defaults) {
+ if (!cloned.hasOwnProperty(key)) {
+ cloned[key] = defaults[key];
+ }
+ }
+
+ return cloned;
+ }
+};
+
+
+
+
+// Domain Public by Eric Wendelin http://eriwen.com/ (2008)
+// Luke Smith http://lucassmith.name/ (2008)
+// Loic Dachary (2008)
+// Johan Euphrosine (2008)
+// Øyvind Sean Kinsey http://kinsey.no/blog (2010)
+//
+// Information and discussions
+// http://jspoker.pokersource.info/skin/test-printstacktrace.html
+// http://eriwen.com/javascript/js-stack-trace/
+// http://eriwen.com/javascript/stacktrace-update/
+// http://pastie.org/253058
+// http://browsershots.org/http://jspoker.pokersource.info/skin/test-printstacktrace.html
+//
+//
+// guessFunctionNameFromLines comes from firebug
+//
+// Software License Agreement (BSD License)
+//
+// Copyright (c) 2007, Parakey Inc.
+// All rights reserved.
+//
+// Redistribution and use of this software in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// * Redistributions of source code must retain the above
+// copyright notice, this list of conditions and the
+// following disclaimer.
+//
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the
+// following disclaimer in the documentation and/or other
+// materials provided with the distribution.
+//
+// * Neither the name of Parakey Inc. nor the names of its
+// contributors may be used to endorse or promote products
+// derived from this software without specific prior
+// written permission of Parakey Inc.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+// IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+function printStackTrace(a){var b=a&&a.e?a.e:null;a=a?!!a.guess:true;var c=new printStackTrace.implementation;b=c.run(b);return a?c.guessFunctions(b):b}printStackTrace.implementation=function(){};
+printStackTrace.implementation.prototype={run:function(a){var b=this._mode||this.mode();if(b==="other")return this.other(arguments.callee);else{var c;if(!(c=a))a:{try{0()}catch(d){c=d;break a}c=void 0}a=c;return this[b](a)}},mode:function(){try{0()}catch(a){if(a.arguments)return this._mode="chrome";else if(a.stack)return this._mode="firefox";else if(window.opera&&!("stacktrace"in a))return this._mode="opera"}return this._mode="other"},chrome:function(a){return a.stack.replace(/^.*?\n/,"").replace(/^.*?\n/,
+"").replace(/^.*?\n/,"").replace(/^[^\(]+?[\n$]/gm,"").replace(/^\s+at\s+/gm,"").replace(/^Object.\s*\(/gm,"{anonymous}()@").split("\n")},firefox:function(a){return a.stack.replace(/^.*?\n/,"").replace(/(?:\n@:0)?\s+$/m,"").replace(/^\(/gm,"{anonymous}(").split("\n")},opera:function(a){a=a.message.split("\n");var b=/Line\s+(\d+).*?script\s+(http\S+)(?:.*?in\s+function\s+(\S+))?/i,c,d,e;c=4;d=0;for(e=a.length;c