Skip to content

Embed URL

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Patched Hoptoad/Airbrake notifier.js to avoid an error when jQuery.Event hits window.onerror
var Hoptoad = {
VERSION : '0.1.0',
NOTICE_XML : '<?xml version="1.0" encoding="UTF-8"?><notice version="2.0"><api-key></api-key><notifier><name>hoptoad_notifier_js</name><version>0.1.0</version><url>http://hoptoadapp.com</url></notifier><error><class>EXCEPTION_CLASS</class><message>EXCEPTION_MESSAGE</message><backtrace>BACKTRACE_LINES</backtrace></error><request><url>REQUEST_URL</url><component>REQUEST_COMPONENT</component><action>REQUEST_ACTION</action></request><server-environment><project-root>PROJECT_ROOT</project-root><environment-name>production</environment-name></server-environment></notice>',
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>.*<\/environment-name>/;
Hoptoad.NOTICE_XML = Hoptoad.NOTICE_XML.replace(matcher,
'<environment-name>' +
value +
'</environment-name>')
},
setHost: function(value) {
Hoptoad.host = value;
},
setKey: function(value) {
var matcher = /<api-key>.*<\/api-key>/;
Hoptoad.NOTICE_XML = Hoptoad.NOTICE_XML.replace(matcher,
'<api-key>' +
value +
'</api-key>');
},
setErrorDefaults: function(value) {
Hoptoad.errorDefaults = value;
},
generateXML: function(errorWithoutDefaults) {
var error = Hoptoad.mergeDefault(Hoptoad.errorDefaults, errorWithoutDefaults);
var xml = Hoptoad.NOTICE_XML;
var baseUrl = error.url || '';
var hash = location.hash || '';
var url = Hoptoad.escapeText((baseUrl + hash) || '');
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>.*<\/request>/, '');
} else {
var data = '';
var cgi_data = error['cgi-data'] || {};
cgi_data["HTTP_USER_AGENT"] = navigator.userAgent;
data += '<cgi-data>';
data += Hoptoad.generateVariables(cgi_data);
data += '</cgi-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('</request>', data + '</request>')
.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('<line method="" file="internal: " number=""/>');
}
}
backtrace.push('<line method="' + Hoptoad.escapeText(matches[1]) +
'" file="' + Hoptoad.escapeText(file) +
'" number="' + matches[3] + '" />');
}
}
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 += '<var key="' + Hoptoad.escapeText(key) + '">' +
Hoptoad.escapeText(parameters[key]) +
'</var>';
}
return result;
},
escapeText: function(text) {
return text.replace(/&/g, '&#38;')
.replace(/</g, '&#60;')
.replace(/>/g, '&#62;')
.replace(/'/g, '&#39;')
.replace(/"/g, '&#34;');
},
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 <loic@dachary.org> (2008)
// Johan Euphrosine <proppy@aminche.com> (2008)
// Oyvind Sean Kinsey http://kinsey.no/blog (2010)
// Victor Homyakov <victor-homyakov@users.sourceforge.net> (2010)
/**
* Main function giving a function stack trace with a forced or passed in Error
*
* @cfg {Error} e The error to create a stacktrace from (optional)
* @cfg {Boolean} guess If we should try to resolve the names of anonymous functions
* @return {Array} of Strings with functions, lines, files, and arguments where possible
*/
function printStackTrace(options) {
options = options || {guess: true};
var ex = options.e || null, guess = !!options.guess;
var p = new printStackTrace.implementation(), result = p.run(ex);
return (guess) ? p.guessAnonymousFunctions(result) : result;
}
printStackTrace.implementation = function() {
};
printStackTrace.implementation.prototype = {
run: function(ex) {
ex = ex || this.createException();
// Do not use the stored mode: different exceptions in Chrome
// may or may not have arguments or stack
var mode = this.mode(ex);
// Use either the stored mode, or resolve it
//var mode = this._mode || this.mode(ex);
if (mode === 'other') {
return this.other(arguments.callee);
} else {
return this[mode](ex);
}
},
createException: function() {
try {
this.undef();
return null;
} catch (e) {
return e;
}
},
/**
* @return {String} mode of operation for the environment in question.
*/
mode: function(e) {
if (e['arguments'] && e.stack) {
return (this._mode = 'chrome');
} else if (e.message && typeof window !== 'undefined' && window.opera) {
return (this._mode = e.stacktrace ? 'opera10' : 'opera');
} else if (e.stack) {
return (this._mode = 'firefox');
}
return (this._mode = 'other');
},
/**
* Given a context, function name, and callback function, overwrite it so that it calls
* printStackTrace() first with a callback and then runs the rest of the body.
*
* @param {Object} context of execution (e.g. window)
* @param {String} functionName to instrument
* @param {Function} function to call with a stack trace on invocation
*/
instrumentFunction: function(context, functionName, callback) {
context = context || window;
var original = context[functionName];
context[functionName] = function instrumented() {
callback.call(this, printStackTrace().slice(4));
return context[functionName]._instrumented.apply(this, arguments);
};
context[functionName]._instrumented = original;
},
/**
* Given a context and function name of a function that has been
* instrumented, revert the function to it's original (non-instrumented)
* state.
*
* @param {Object} context of execution (e.g. window)
* @param {String} functionName to de-instrument
*/
deinstrumentFunction: function(context, functionName) {
if (context[functionName].constructor === Function &&
context[functionName]._instrumented &&
context[functionName]._instrumented.constructor === Function) {
context[functionName] = context[functionName]._instrumented;
}
},
/**
* Given an Error object, return a formatted Array based on Chrome's stack string.
*
* @param e - Error object to inspect
* @return Array<String> of function calls, files and line numbers
*/
chrome: function(e) {
var stack = (e.stack + '\n').replace(/^\S[^\(]+?[\n$]/gm, '').
replace(/^\s+at\s+/gm, '').
replace(/^([^\(]+?)([\n$])/gm, '{anonymous}()@$1$2').
replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm, '{anonymous}()@$1').split('\n');
stack.pop();
return stack;
},
/**
* Given an Error object, return a formatted Array based on Firefox's stack string.
*
* @param e - Error object to inspect
* @return Array<String> of function calls, files and line numbers
*/
firefox: function(e) {
return e.stack.replace(/(?:\n@:0)?\s+$/m, '').replace(/^\(/gm, '{anonymous}(').split('\n');
},
/**
* Given an Error object, return a formatted Array based on Opera 10's stacktrace string.
*
* @param e - Error object to inspect
* @return Array<String> of function calls, files and line numbers
*/
opera10: function(e) {
var stack = e.stacktrace;
var lines = stack.split('\n'), ANON = '{anonymous}', lineRE = /.*line (\d+), column (\d+) in ((<anonymous function\:?\s*(\S+))|([^\(]+)\([^\)]*\))(?: in )?(.*)\s*$/i, i, j, len;
for (i = 2, j = 0, len = lines.length; i < len - 2; i++) {
if (lineRE.test(lines[i])) {
var location = RegExp.$6 + ':' + RegExp.$1 + ':' + RegExp.$2;
var fnName = RegExp.$3;
fnName = fnName.replace(/<anonymous function\:?\s?(\S+)?>/g, ANON);
lines[j++] = fnName + '@' + location;
}
}
lines.splice(j, lines.length - j);
return lines;
},
// Opera 7.x-9.x only!
opera: function(e) {
var lines = e.message.split('\n'), ANON = '{anonymous}', lineRE = /Line\s+(\d+).*script\s+(http\S+)(?:.*in\s+function\s+(\S+))?/i, i, j, len;
for (i = 4, j = 0, len = lines.length; i < len; i += 2) {
//TODO: RegExp.exec() would probably be cleaner here
if (lineRE.test(lines[i])) {
lines[j++] = (RegExp.$3 ? RegExp.$3 + '()@' + RegExp.$2 + RegExp.$1 : ANON + '()@' + RegExp.$2 + ':' + RegExp.$1) + ' -- ' + lines[i + 1].replace(/^\s+/, '');
}
}
lines.splice(j, lines.length - j);
return lines;
},
// Safari, IE, and others
other: function(curr) {
var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i, stack = [], fn, args, maxStackSize = 10;
while (curr && stack.length < maxStackSize) {
fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON;
args = Array.prototype.slice.call(curr['arguments'] || []);
stack[stack.length] = fn + '(' + this.stringifyArguments(args) + ')';
curr = curr.caller;
}
return stack;
},
/**
* Given arguments array as a String, subsituting type names for non-string types.
*
* @param {Arguments} object
* @return {Array} of Strings with stringified arguments
*/
stringifyArguments: function(args) {
var slice = Array.prototype.slice;
for (var i = 0; i < args.length; ++i) {
var arg = args[i];
if (arg === undefined) {
args[i] = 'undefined';
} else if (arg === null) {
args[i] = 'null';
} else if (arg.constructor) {
if (arg.constructor === Array) {
if (arg.length < 3) {
args[i] = '[' + this.stringifyArguments(arg) + ']';
} else {
args[i] = '[' + this.stringifyArguments(slice.call(arg, 0, 1)) + '...' + this.stringifyArguments(slice.call(arg, -1)) + ']';
}
} else if (arg.constructor === Object) {
args[i] = '#object';
} else if (arg.constructor === Function) {
args[i] = '#function';
} else if (arg.constructor === String) {
args[i] = '"' + arg + '"';
}
}
}
return args.join(',');
},
sourceCache: {},
/**
* @return the text from a given URL.
*/
ajax: function(url) {
var req = this.createXMLHTTPObject();
if (!req) {
return;
}
req.open('GET', url, false);
req.setRequestHeader('User-Agent', 'XMLHTTP/1.0');
req.send('');
return req.responseText;
},
/**
* Try XHR methods in order and store XHR factory.
*
* @return <Function> XHR function or equivalent
*/
createXMLHTTPObject: function() {
var xmlhttp, XMLHttpFactories = [
function() {
return new XMLHttpRequest();
}, function() {
return new ActiveXObject('Msxml2.XMLHTTP');
}, function() {
return new ActiveXObject('Msxml3.XMLHTTP');
}, function() {
return new ActiveXObject('Microsoft.XMLHTTP');
}
];
for (var i = 0; i < XMLHttpFactories.length; i++) {
try {
xmlhttp = XMLHttpFactories[i]();
// Use memoization to cache the factory
this.createXMLHTTPObject = XMLHttpFactories[i];
return xmlhttp;
} catch (e) {
}
}
},
/**
* Given a URL, check if it is in the same domain (so we can get the source
* via Ajax).
*
* @param url <String> source url
* @return False if we need a cross-domain request
*/
isSameDomain: function(url) {
return url.indexOf(location.hostname) !== -1;
},
/**
* Get source code from given URL if in the same domain.
*
* @param url <String> JS source URL
* @return <Array> Array of source code lines
*/
getSource: function(url) {
if (!(url in this.sourceCache)) {
this.sourceCache[url] = this.ajax(url).split('\n');
}
return this.sourceCache[url];
},
guessAnonymousFunctions: function(stack) {
for (var i = 0; i < stack.length; ++i) {
var reStack = /\{anonymous\}\(.*\)@(\w+:\/\/([\-\w\.\/]+)+(:\d+)?[^:]+):(\d+):?(\d+)?/;
var frame = stack[i], m = reStack.exec(frame);
if (m) {
var file = m[1], lineno = m[4], charno = m[7] || 0; //m[7] is character position in Chrome
if (file && this.isSameDomain(file) && lineno) {
var functionName = this.guessAnonymousFunction(file, lineno, charno);
stack[i] = frame.replace('{anonymous}', functionName);
}
}
}
return stack;
},
guessAnonymousFunction: function(url, lineNo, charNo) {
var ret;
try {
ret = this.findFunctionName(this.getSource(url), lineNo);
} catch (e) {
ret = 'getSource failed with url: ' + url + ', exception: ' + e.toString();
}
return ret;
},
findFunctionName: function(source, lineNo) {
// FIXME findFunctionName fails for compressed source
// (more than one function on the same line)
// TODO use captured args
// function {name}({args}) m[1]=name m[2]=args
var reFunctionDeclaration = /function\s+([^(]*?)\s*\(([^)]*)\)/;
// {name} = function ({args}) TODO args capture
// /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function(?:[^(]*)/
var reFunctionExpression = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function\b/;
// {name} = eval()
var reFunctionEvaluation = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(?:eval|new Function)\b/;
// Walk backwards in the source lines until we find
// the line which matches one of the patterns above
var code = "", line, maxLines = 10, m;
for (var i = 0; i < maxLines; ++i) {
// FIXME lineNo is 1-based, source[] is 0-based
line = source[lineNo - i];
if (line) {
code = line + code;
m = reFunctionExpression.exec(code);
if (m && m[1]) {
return m[1];
}
m = reFunctionDeclaration.exec(code);
if (m && m[1]) {
//return m[1] + "(" + (m[2] || "") + ")";
return m[1];
}
m = reFunctionEvaluation.exec(code);
if (m && m[1]) {
return m[1];
}
}
}
return '(?)';
}
};
window.onerror = function(message, file, line) {
if(jQuery !== undefined && jQuery.Event !== undefined && message instanceof jQuery.Event) {
return true;
}
setTimeout(function() {
Hoptoad.notify({
message : message,
stack : '()@' + file + ':' + line
});
}, 100);
return true;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.