Created
May 13, 2020 21:17
-
-
Save jclif/35e2eafec60397952829c88fc1d040e6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
honeybadger.js v0.5.5 | |
A JavaScript Notifier for Honeybadger | |
https://github.com/honeybadger-io/honeybadger-js | |
https://www.honeybadger.io/ | |
MIT license | |
*/ | |
(function (root, builder) { | |
'use strict'; | |
// Read default configuration from script tag if available. | |
var scriptConfig = {}; | |
(function() { | |
var tags = document.getElementsByTagName("script"); | |
var tag = tags[tags.length - 1]; | |
if (!tag) { return; } | |
var attrs = tag.attributes; | |
var value; | |
for (var i = 0, len = attrs.length; i < len; i++) { | |
if (/data-(\w+)$/.test(attrs[i].nodeName)) { | |
value = attrs[i].nodeValue; | |
if (value === 'false') { value = false; } | |
scriptConfig[RegExp.$1] = value; | |
} | |
} | |
})(); | |
// Build the singleton factory. The factory can be accessed through | |
// singleton.factory() to instantiate a new instance. | |
var factory = function(){ | |
var f = builder(); | |
var singleton = f(scriptConfig); | |
singleton.factory = f; | |
return singleton; | |
}; | |
// UMD (Universal Module Definition) | |
// See https://github.com/umdjs/umd | |
if (typeof define === 'function' && define.amd) { | |
// AMD. Register as an anonymous module. | |
define([], factory); | |
} else if (typeof module === 'object' && module.exports) { | |
// Browserify. Does not work with strict CommonJS, but | |
// only CommonJS-like environments that support module.exports, | |
// like Browserfy/Node. | |
module.exports = factory(); | |
} else { | |
// Browser globals (root is window). | |
root.Honeybadger = factory(); | |
} | |
}(typeof self !== 'undefined' ? self : this, function () { | |
var VERSION = '0.5.5', | |
NOTIFIER = { | |
name: 'honeybadger.js', | |
url: 'https://github.com/honeybadger-io/honeybadger-js', | |
version: VERSION, | |
language: 'javascript' | |
}; | |
// Used to control initial setup across clients. | |
var loaded = false, | |
installed = false; | |
// Used to prevent reporting duplicate errors across instances. | |
var currentErr, | |
currentPayload; | |
// Utilities. | |
function merge(obj1, obj2) { | |
var obj3 = {}; | |
for (var k in obj1) { obj3[k] = obj1[k]; } | |
for (var k in obj2) { obj3[k] = obj2[k]; } | |
return obj3; | |
} | |
function currentErrIs(err) { | |
if (!currentErr) { return false; } | |
if (currentErr.name !== err.name) { return false; } | |
if (currentErr.message !== err.message) { return false; } | |
if (currentErr.stack !== err.stack) { return false; } | |
return true; | |
} | |
function isIgnored(err, patterns) { | |
var msg = err.message; | |
for (var p in patterns) { | |
if (msg.match(patterns[p])) { return true; } | |
} | |
return false; | |
} | |
function cgiData() { | |
var data = {}; | |
data['HTTP_USER_AGENT'] = navigator.userAgent; | |
if (document.referrer.match(/\S/)) { | |
data['HTTP_REFERER'] = document.referrer; | |
} | |
return data; | |
} | |
function encodeCookie(object) { | |
if (typeof object !== 'object') { | |
return undefined; | |
} | |
var cookies = []; | |
for (var k in object) { | |
cookies.push(k + '=' + object[k]); | |
} | |
return cookies.join(';'); | |
} | |
function stackTrace(err) { | |
// From TraceKit: Opera 10 *destroys* its stacktrace property if you try to | |
// access the stack property first. | |
return err.stacktrace || err.stack || undefined | |
} | |
function generateStackTrace(err) { | |
var stack; | |
var maxStackSize = 10; | |
if (err && (stack = stackTrace(err))) { | |
return {stack: stack, generator: undefined}; | |
} | |
try { | |
throw new Error(''); | |
} catch(e) { | |
if (stack = stackTrace(e)) { | |
return {stack: stack, generator: 'throw'}; | |
} | |
} | |
stack = ['<call-stack>']; | |
var curr = arguments.callee; | |
while (curr && stack.length < maxStackSize) { | |
if (/function(?:\s+([\w$]+))+\s*\(/.test(curr.toString())) { | |
stack.push(RegExp.$1 || '<anonymous>'); | |
} else { | |
stack.push('<anonymous>'); | |
} | |
try { | |
curr = curr.caller; | |
} catch (e) { | |
break; | |
} | |
} | |
return {stack: stack.join('\n'), generator: 'walk'}; | |
} | |
function checkHandlers(handlers, err) { | |
var handler, i, len; | |
for (i = 0, len = handlers.length; i < len; i++) { | |
handler = handlers[i]; | |
if (handler(err) === false) { | |
return true; | |
} | |
} | |
return false; | |
} | |
// Client factory. | |
var factory = (function(opts) { | |
var defaultProps = []; | |
var queue = []; | |
var self = { | |
context: {}, | |
beforeNotifyHandlers: [], | |
errorsSent: 0 | |
} | |
if (typeof opts === 'object') { | |
for (var k in opts) { self[k] = opts[k]; } | |
} | |
function log() { | |
var console = window.console; | |
if (console) { | |
var args = Array.prototype.slice.call(arguments); | |
args.unshift('[Honeybadger]'); | |
console.log.apply(console, args); | |
} | |
} | |
function debug() { | |
if (config('debug')) { | |
return log.apply(this, arguments); | |
} | |
} | |
function config(key, fallback) { | |
var value = self[key]; | |
if (value === undefined) { value = self[key.toLowerCase()] } | |
if (value === 'false') { value = false; } | |
if (value !== undefined) { return value; } | |
return fallback; | |
} | |
function baseURL() { | |
return 'http' + ((config('ssl', true) && 's') || '') + '://' + config('host', 'api.honeybadger.io'); | |
} | |
function canSerialize(obj) { | |
// Functions are TMI and Symbols can't convert to strings. | |
if (/function|symbol/.test(typeof(obj))) { return false; } | |
// No prototype, likely created with `Object.create(null)`. | |
if (typeof obj === 'object' && typeof obj.hasOwnProperty === 'undefined') { return false; } | |
return true; | |
} | |
function serialize(obj, prefix, depth) { | |
var k, pk, ret, v; | |
ret = []; | |
if (!depth) { depth = 0; } | |
if (depth >= config('max_depth', 8)) { | |
return encodeURIComponent(prefix) + '=[MAX DEPTH REACHED]'; | |
} | |
for (k in obj) { | |
v = obj[k]; | |
if (obj.hasOwnProperty(k) && (k != null) && (v != null)) { | |
if (!canSerialize(v)) { v = Object.prototype.toString.call(v); } | |
pk = (prefix ? prefix + '[' + k + ']' : k); | |
ret.push(typeof v === 'object' ? serialize(v, pk, depth+1) : encodeURIComponent(pk) + '=' + encodeURIComponent(v)); | |
} | |
} | |
return ret.join('&'); | |
} | |
function request(url) { | |
if (config('disabled', false)) { return false; } | |
if (exceedsMaxErrors()) { return false; } | |
// Use XHR when available. | |
try { | |
// Inspired by https://gist.github.com/Xeoncross/7663273 | |
var x = new(window.XMLHttpRequest || ActiveXObject)('MSXML2.XMLHTTP.3.0'); | |
x.open('GET', url, config('async', true)); | |
x.send(); | |
incrementErrorsCount(); | |
return; | |
} catch(e) { | |
log('Error encountered during XHR request (will retry): ' + e); | |
} | |
// Fall back to Image transport. | |
var img = new Image(); | |
img.src = url; | |
} | |
function send(payload) { | |
currentErr = currentPayload = null; | |
var apiKey = config('apiKey', config('api_key')); | |
if (!apiKey) { | |
log('Unable to send error report: no API key has been configured.'); | |
return false; | |
} | |
var url = baseURL() + '/v1/notices/js.gif?' + serialize({'notice': payload}) + | |
'&api_key=' + apiKey + '&t=' + new Date().getTime(); | |
request(url); | |
return true; | |
} | |
function notify(err, generated) { | |
if (!err) { err = {}; } | |
if (Object.prototype.toString.call(err) === '[object Error]') { | |
var e = err; | |
err = merge(err, {name: e.name, message: e.message, stack: stackTrace(e)}) | |
} | |
if (!(typeof err === 'object')) { | |
var m = String(err); | |
err = {message: m}; | |
} | |
if (currentErrIs(err)) { | |
// Skip the duplicate error. | |
return false; | |
} else if (currentPayload && loaded) { | |
// This is a different error, send the old one now. | |
send(currentPayload); | |
} | |
// Halt if err is empty. | |
if (((function() { | |
var k, results; | |
results = []; | |
for (k in err) { | |
if (!{}.hasOwnProperty.call(err, k)) continue; | |
results.push(k); | |
} | |
return results; | |
})()).length === 0) { | |
return false; | |
} | |
if (generated) { | |
err = merge(err, generated); | |
} | |
if (isIgnored(err, config('ignorePatterns'))) { return false; } | |
if (checkHandlers(self.beforeNotifyHandlers, err)) { return false; } | |
var data = cgiData(); | |
if (typeof err.cookies === 'string') { | |
data['HTTP_COOKIE'] = err.cookies; | |
} else if (typeof err.cookies === 'object') { | |
data['HTTP_COOKIE'] = encodeCookie(err.cookies); | |
} | |
var payload = { | |
'notifier': NOTIFIER, | |
'error': { | |
'class': err.name || 'Error', | |
'message': err.message, | |
'backtrace': err.stack, | |
'generator': err.generator, | |
'fingerprint': err.fingerprint | |
}, | |
'request': { | |
'url': err.url || document.URL, | |
'component': err.component || config('component'), | |
'action': err.action || config('action'), | |
'context': merge(self.context, err.context), | |
'cgi_data': data, | |
'params': err.params | |
}, | |
'server': { | |
'project_root': err.projectRoot || err.project_root || config('projectRoot', config('project_root', window.location.protocol + '//' + window.location.host)), | |
'environment_name': err.environment || config('environment'), | |
'revision': err.revision || config('revision') | |
} | |
}; | |
currentPayload = payload; | |
currentErr = err; | |
if (loaded) { | |
debug('Deferring notice.', err, payload); | |
window.setTimeout(function(){ | |
if (currentErrIs(err)) { | |
send(payload); | |
} | |
}); | |
} else { | |
debug('Queuing notice.', err, payload); | |
queue.push(payload); | |
} | |
return err; | |
} | |
function objectIsExtensible(obj) { | |
if (typeof Object.isExtensible !== 'function') { return true; } | |
return Object.isExtensible(obj); | |
} | |
var preferCatch = true; | |
// IE < 10 | |
if (!window.atob) { preferCatch = false; } | |
// See https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent | |
if (window.ErrorEvent) { | |
try { | |
if ((new window.ErrorEvent('')).colno === 0) { | |
preferCatch = false; | |
} | |
} catch(_e) {} | |
} | |
// wrap always returns the same function so that callbacks can be removed via | |
// removeEventListener. | |
function wrap(fn, force) { | |
try { | |
if (typeof fn !== 'function') { return fn; } | |
if (!objectIsExtensible(fn)) { return fn; } | |
if (!fn.___hb) { | |
fn.___hb = function() { | |
var onerror = config('onerror', true); | |
// Don't catch if the browser is old or supports the new error | |
// object and there is a window.onerror handler available instead. | |
if ((preferCatch && (onerror || force)) || (force && !onerror)) { | |
try { | |
return fn.apply(this, arguments); | |
} catch (e) { | |
notify(e); | |
throw(e); | |
} | |
} else { | |
return fn.apply(this, arguments); | |
} | |
}; | |
} | |
fn.___hb.___hb = fn.___hb; | |
return fn.___hb; | |
} catch(_e) { | |
return fn; | |
} | |
} | |
// Public API. | |
self.notify = function(err, name, extra) { | |
if (!err) { err = {}; } | |
if (Object.prototype.toString.call(err) === '[object Error]') { | |
var e = err; | |
err = merge(err, {name: e.name, message: e.message, stack: stackTrace(e)}) | |
} | |
if (!(typeof err === 'object')) { | |
var m = String(err); | |
err = {message: m}; | |
} | |
if (name && !(typeof name === 'object')) { | |
var n = String(name); | |
name = {name: n}; | |
} | |
if (name) { | |
err = merge(err, name); | |
} | |
if (typeof extra === 'object') { | |
err = merge(err, extra); | |
} | |
return notify(err, generateStackTrace(err)); | |
}; | |
self.wrap = function(func) { | |
return wrap(func, true); | |
}; | |
self.setContext = function(context) { | |
if (typeof context === 'object') { | |
self.context = merge(self.context, context); | |
} | |
return self; | |
}; | |
self.resetContext = function(context) { | |
if (typeof context === 'object') { | |
self.context = merge({}, context); | |
} else { | |
self.context = {}; | |
} | |
return self; | |
}; | |
self.configure = function(opts) { | |
for (var k in opts) { | |
self[k] = opts[k]; | |
} | |
return self; | |
}; | |
self.beforeNotify = function(handler) { | |
self.beforeNotifyHandlers.push(handler); | |
return self; | |
}; | |
var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; | |
self.reset = function() { | |
self.context = {}; | |
self.beforeNotifyHandlers = []; | |
for (var k in self) { | |
if (indexOf.call(defaultProps, k) == -1) { | |
self[k] = undefined; | |
} | |
} | |
self.resetMaxErrors(); | |
return self; | |
}; | |
self.resetMaxErrors = function() { | |
return (self.errorsSent=0); | |
}; | |
self.getVersion = function() { | |
return VERSION; | |
} | |
// Install instrumentation. | |
// This should happen once for the first factory call. | |
function instrument(object, name, replacement) { | |
if (installed) { return; } | |
if (!object || !name || !replacement) { return; } | |
var original = object[name]; | |
object[name] = replacement(original); | |
} | |
var instrumentTimer = function(original) { | |
// See https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout | |
return function(func, delay) { | |
if (typeof func === 'function') { | |
var args = Array.prototype.slice.call(arguments, 2); | |
func = wrap(func); | |
return original(function() { | |
func.apply(null, args); | |
}, delay); | |
} else { | |
return original(func, delay); | |
} | |
} | |
}; | |
instrument(window, 'setTimeout', instrumentTimer); | |
instrument(window, 'setInterval', instrumentTimer); | |
// Event targets borrowed from bugsnag-js: | |
// See https://github.com/bugsnag/bugsnag-js/blob/d55af916a4d3c7757f979d887f9533fe1a04cc93/src/bugsnag.js#L542 | |
'EventTarget Window Node ApplicationCache AudioTrackList ChannelMergerNode CryptoOperation EventSource FileReader HTMLUnknownElement IDBDatabase IDBRequest IDBTransaction KeyOperation MediaController MessagePort ModalWindow Notification SVGElementInstance Screen TextTrack TextTrackCue TextTrackList WebSocket WebSocketWorker Worker XMLHttpRequest XMLHttpRequestEventTarget XMLHttpRequestUpload'.replace(/\w+/g, function (prop) { | |
var prototype = window[prop] && window[prop].prototype; | |
if (prototype && prototype.hasOwnProperty && prototype.hasOwnProperty('addEventListener')) { | |
instrument(prototype, 'addEventListener', function(original) { | |
// See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener | |
return function(type, listener, useCapture, wantsUntrusted) { | |
try { | |
if (listener && listener.handleEvent != null) { | |
listener.handleEvent = wrap(listener.handleEvent); | |
} | |
} catch(e) { | |
// Ignore 'Permission denied to access property "handleEvent"' errors. | |
log(e); | |
} | |
return original.call(this, type, wrap(listener), useCapture, wantsUntrusted); | |
}; | |
}); | |
instrument(prototype, 'removeEventListener', function(original) { | |
return function(type, listener, useCapture, wantsUntrusted) { | |
original.call(this, type, listener, useCapture, wantsUntrusted); | |
return original.call(this, type, wrap(listener), useCapture, wantsUntrusted); | |
}; | |
}); | |
} | |
}); | |
instrument(window, 'onerror', function(original) { | |
function onerror(msg, url, line, col, err) { | |
debug('window.onerror callback invoked.', arguments); | |
// Skip if the error is already being sent. | |
if (currentErr) { return; } | |
if (!config('onerror', true)) { return; } | |
if (line === 0 && /Script error\.?/.test(msg)) { | |
// See https://developer.mozilla.org/en/docs/Web/API/GlobalEventHandlers/onerror#Notes | |
log('Ignoring cross-domain script error. Use CORS to enable tracking of these types of errors.', arguments); | |
return; | |
} | |
// simulate v8 stack | |
var stack = [msg, '\n at ? (', url || 'unknown', ':', line || 0, ':', col || 0, ')'].join(''); | |
if (err) { | |
var generated = { stack: stackTrace(err) }; | |
if (!generated.stack) { generated = {stack: stack}; } | |
notify(err, generated); | |
return; | |
} | |
notify({ | |
name: 'window.onerror', | |
message: msg, | |
stack: stack | |
}); | |
} | |
// See https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror | |
return function(msg, url, line, col, err) { | |
onerror(msg, url, line, col, err); | |
if (typeof original === 'function') { | |
return original.apply(this, arguments); | |
} | |
return false; | |
}; | |
}); | |
function incrementErrorsCount() { | |
return self.errorsSent++; | |
} | |
function exceedsMaxErrors() { | |
var maxErrors = config('maxErrors'); | |
return maxErrors && self.errorsSent >= maxErrors; | |
} | |
// End of instrumentation. | |
installed = true; | |
// Save original state for reset() | |
for (var k in self) { | |
defaultProps.push(k); | |
} | |
// Initialization. | |
debug('Initializing honeybadger.js ' + VERSION); | |
// See https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState | |
// https://www.w3.org/TR/html5/dom.html#dom-document-readystate | |
// The 'loaded' state is for older versions of Safari. | |
if (/complete|interactive|loaded/.test(document.readyState)) { | |
loaded = true; | |
debug('honeybadger.js ' + VERSION + ' ready'); | |
} else { | |
debug('Installing ready handler'); | |
var domReady = function() { | |
loaded = true; | |
debug('honeybadger.js ' + VERSION + ' ready'); | |
var notice; | |
while (notice = queue.pop()) { | |
send(notice); | |
} | |
}; | |
if (document.addEventListener) { | |
document.addEventListener('DOMContentLoaded', domReady, true); | |
} else { | |
window.attachEvent('onload', domReady); | |
} | |
} | |
return self; | |
}); | |
return factory; | |
})); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment