honeybadger-js
Advanced tools
Comparing version 2.0.0 to 2.1.0-beta.0
@@ -8,2 +8,8 @@ # Change Log | ||
## [2.1.0-beta.0] - 2019-11-13 | ||
### Added | ||
- Add Breadcrumbs (#210). See [the | ||
docs](https://docs.honeybadger.io/lib/javascript/guides/breadcrumbs.html) | ||
for more info. | ||
## [2.0.0] - 2019-07-24 | ||
@@ -10,0 +16,0 @@ ### Added |
'use strict'; | ||
function sanitize(obj, maxDepth) { | ||
let seenObjects = []; | ||
function seen(obj) { | ||
if (!obj || typeof(obj) !== 'object') { return false; } | ||
for (let i = 0; i < seenObjects.length; i++) { | ||
const value = seenObjects[i]; | ||
if (value === obj) { | ||
return true; | ||
} | ||
} | ||
seenObjects.push(obj); | ||
return false; | ||
} | ||
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, depth) { | ||
if (!depth) { depth = 0; } | ||
if (depth >= maxDepth) { | ||
return '[DEPTH]'; | ||
} | ||
// Inspect invalid types | ||
if (!canSerialize(obj)) { return Object.prototype.toString.call(obj); } | ||
// Halt circular references | ||
if (seen(obj)) { | ||
return '[RECURSION]'; | ||
} | ||
// Serialize inside arrays | ||
if (Array.isArray(obj)) { | ||
return obj.map(o => serialize(o, depth+1)); | ||
} | ||
// Serialize inside objects | ||
if (typeof(obj) === 'object') { | ||
let ret = {}; | ||
for (const k in obj) { | ||
const v = obj[k]; | ||
if (Object.prototype.hasOwnProperty.call(obj, k) && (k != null) && (v != null)) { | ||
ret[k] = serialize(v, depth+1); | ||
} | ||
} | ||
return ret; | ||
} | ||
// Return everything else untouched | ||
return obj; | ||
} | ||
return serialize(obj); | ||
} | ||
/** | ||
* Converts an HTMLElement into a human-readable string. | ||
* @param {!HTMLElement} element | ||
* @return {string} | ||
*/ | ||
function stringNameOfElement(element) { | ||
if (!element || !element.tagName) { return ''; } | ||
let name = element.tagName.toLowerCase(); | ||
// Ignore the root <html> element in selectors and events. | ||
if (name === 'html') { return ''; } | ||
if (element.id) { | ||
name += `#${element.id}`; | ||
} | ||
const stringClassNames = element.getAttribute('class'); | ||
if (stringClassNames) { | ||
stringClassNames.split(/\s+/).forEach(className => { | ||
name += `.${className}`; | ||
}); | ||
} | ||
['alt', 'name', 'title', 'type'].forEach(attrName => { | ||
let attr = element.getAttribute(attrName); | ||
if (attr) { | ||
name += `[${attrName}="${attr}"]`; | ||
} | ||
}); | ||
const siblings = getSiblings(element); | ||
if (siblings.length > 1) { | ||
name += `:nth-child(${Array.prototype.indexOf.call(siblings, element) + 1})`; | ||
} | ||
return name; | ||
} | ||
function stringSelectorOfElement(element) { | ||
const name = stringNameOfElement(element); | ||
if (element.parentNode && element.parentNode.tagName) { | ||
const parentName = stringSelectorOfElement(element.parentNode); | ||
if (parentName.length > 0) { | ||
return `${parentName} > ${name}`; | ||
} | ||
} | ||
return name; | ||
} | ||
function stringTextOfElement(element) { | ||
return truncate((element.textContent || element.innerText || element.value || '').trim(), 300); | ||
} | ||
function nativeFetch() { | ||
if (!window.fetch) { return false; } | ||
if (isNative(window.fetch)) { return true; } | ||
// If fetch isn't native, it may be wrapped by someone else. Try to get | ||
// a pristine function from an iframe. | ||
try { | ||
const sandbox = document.createElement('iframe'); | ||
sandbox.style.display = 'none'; | ||
document.head.appendChild(sandbox); | ||
const result = sandbox.contentWindow.fetch && isNative(sandbox.contentWindow.fetch); | ||
document.head.removeChild(sandbox); | ||
return result; | ||
} catch(err) { | ||
if (console && console.warn) { | ||
console.warn('failed to detect native fetch via iframe: ' + err); | ||
} | ||
} | ||
return false; | ||
} | ||
function isNative(func) { | ||
return func.toString().indexOf('native') !== -1; | ||
} | ||
function parseURL(url) { | ||
// Regexp: https://tools.ietf.org/html/rfc3986#appendix-B | ||
const match = url.match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/) || {}; | ||
return { | ||
protocol: match[2], | ||
host: match[4], | ||
pathname: match[5], | ||
}; | ||
} | ||
function localURLPathname(url) { | ||
const parsed = parseURL(url); | ||
const parsedDocURL = parseURL(document.URL); | ||
// URL must be relative | ||
if (!parsed.host || parsed.protocol) { return parsed.pathname; } | ||
// Same domain | ||
if (parsed.protocol === parsedDocURL.protocol && parsed.host === parsedDocURL.host) { | ||
return parsed.pathname; | ||
} | ||
// x-domain | ||
return `${parsed.protocol}://${parsed.host}${parsed.pathname}`; | ||
} | ||
// Helpers | ||
function getSiblings(element) { | ||
try { | ||
const nodes = element.parentNode.childNodes; | ||
const siblings = []; | ||
Array.prototype.forEach.call(nodes, node => { | ||
if (node.tagName && node.tagName === element.tagName) { | ||
siblings.push(node); | ||
} | ||
}); | ||
return siblings; | ||
} catch(e) { | ||
return []; | ||
} | ||
} | ||
function truncate(string, length) { | ||
if (string.length > length) { | ||
string = string.substr(0, length) + '...'; | ||
} | ||
return string; | ||
} | ||
function builder() { | ||
var VERSION = '2.0.0', | ||
var VERSION = '2.1.0-beta.0', | ||
NOTIFIER = { | ||
name: 'honeybadger.js', | ||
name: 'honeybadger-js', | ||
url: 'https://github.com/honeybadger-io/honeybadger-js', | ||
@@ -81,3 +279,3 @@ version: VERSION, | ||
// access the stack property first. | ||
return err.stacktrace || err.stack || undefined | ||
return err.stacktrace || err.stack || undefined; | ||
} | ||
@@ -152,3 +350,4 @@ | ||
beforeNotifyHandlers: [], | ||
errorsSent: 0 | ||
breadcrumbs: [], | ||
errorsSent: 0, | ||
}; | ||
@@ -192,2 +391,6 @@ if (typeof opts === 'object') { | ||
function breadcrumbsEnabled() { | ||
return config('breadcrumbsEnabled', false); | ||
} | ||
function baseURL() { | ||
@@ -197,29 +400,2 @@ 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, depth) { | ||
var k, v, ret; | ||
ret = {}; | ||
if (!depth) { depth = 0; } | ||
if (depth >= config('max_depth', 8)) { | ||
return '[MAX DEPTH REACHED]'; | ||
} | ||
for (k in obj) { | ||
v = obj[k]; | ||
if (Object.prototype.hasOwnProperty.call(obj, k) && (k != null) && (v != null)) { | ||
if (!canSerialize(v)) { v = Object.prototype.toString.call(v); } | ||
ret[k] = (typeof v === 'object' ? serialize(v, depth+1) : v); | ||
} | ||
} | ||
return ret; | ||
} | ||
function request(apiKey, payload) { | ||
@@ -234,3 +410,3 @@ try { | ||
x.send(JSON.stringify(serialize(payload))); | ||
x.send(JSON.stringify(sanitize(payload, config('max_depth', 8)))); | ||
} catch(err) { | ||
@@ -307,2 +483,13 @@ log('Unable to send error report: error while initializing request', err, payload); | ||
self.addBreadcrumb('Honeybadger Notice', { | ||
category: 'notice', | ||
metadata: { | ||
message: err.message, | ||
name: err.name, | ||
stack: err.stack | ||
} | ||
}); | ||
err.breadcrumbs = self.breadcrumbs.slice(); | ||
let stack_before_handlers = err.stack; | ||
@@ -326,2 +513,6 @@ if (checkHandlers(self.beforeNotifyHandlers, err)) { return false; } | ||
'notifier': NOTIFIER, | ||
'breadcrumbs': { | ||
'enabled': breadcrumbsEnabled(), | ||
'trail': err.breadcrumbs, | ||
}, | ||
'error': { | ||
@@ -381,3 +572,4 @@ 'class': err.name, | ||
// removeEventListener. | ||
function wrap(fn, force) { | ||
function wrap(fn, opts) { | ||
if (!opts) { opts = {}; } | ||
try { | ||
@@ -391,8 +583,20 @@ if (typeof fn !== 'function') { return fn; } | ||
// object and there is a window.onerror handler available instead. | ||
if ((preferCatch && (onerror || force)) || (force && !onerror)) { | ||
if ((preferCatch && (onerror || opts.force)) || (opts.force && !onerror)) { | ||
try { | ||
return fn.apply(this, arguments); | ||
} catch (e) { | ||
notify(e); | ||
throw(e); | ||
} catch (err) { | ||
let generated = { stack: stackTrace(err) }; | ||
self.addBreadcrumb( | ||
opts.component ? `${opts.component}: ${err.name}` : err.name, | ||
{ | ||
category: 'error', | ||
metadata: { | ||
message: err.message, | ||
name: err.name, | ||
stack: generated.stack | ||
} | ||
} | ||
); | ||
notify(err, generated); | ||
throw(err); | ||
} | ||
@@ -441,3 +645,3 @@ } else { | ||
self.wrap = function(func) { | ||
return wrap(func, true); | ||
return wrap(func, { force: true }); | ||
}; | ||
@@ -477,2 +681,3 @@ | ||
self.beforeNotifyHandlers = []; | ||
self.breadcrumbs = []; | ||
for (var k in self) { | ||
@@ -495,2 +700,26 @@ if (indexOf.call(defaultProps, k) == -1) { | ||
self.addBreadcrumb = function(message, opts) { | ||
if (!breadcrumbsEnabled()) return; | ||
opts = opts || {}; | ||
const metadata = opts.metadata || undefined; | ||
const category = opts.category || 'custom'; | ||
const timestamp = new Date().toISOString(); | ||
self.breadcrumbs.push({ | ||
category: category, | ||
message: message, | ||
metadata: metadata || {}, | ||
timestamp: timestamp, | ||
}); | ||
const limit = config('maxBreadcrumbs', 40); | ||
if (self.breadcrumbs.length > limit) { | ||
self.breadcrumbs = self.breadcrumbs.slice(self.breadcrumbs.length - limit); | ||
} | ||
return self; | ||
}; | ||
// Install instrumentation. | ||
@@ -500,3 +729,3 @@ // This should happen once for the first factory call. | ||
if (notSingleton) { return; } | ||
if (!object || !name || !replacement) { return; } | ||
if (!object || !name || !replacement || !(name in object)) { return; } | ||
var original = object[name]; | ||
@@ -506,19 +735,257 @@ 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); | ||
// Breadcrumbs: instrument click events | ||
(function() { | ||
window.addEventListener('click', (event) => { | ||
let message, selector, text; | ||
try { | ||
message = stringNameOfElement(event.target); | ||
selector = stringSelectorOfElement(event.target); | ||
text = stringTextOfElement(event.target); | ||
} catch(e) { | ||
message = 'UI Click'; | ||
selector = '[unknown]'; | ||
text = '[unknown]'; | ||
} | ||
// There's nothing to display | ||
if (message.length === 0) { return; } | ||
self.addBreadcrumb(message, { | ||
category: 'ui.click', | ||
metadata: { | ||
selector, | ||
text, | ||
event, | ||
}, | ||
}); | ||
}, true); | ||
})(); | ||
// Breadcrumbs: instrument XMLHttpRequest | ||
(function() { | ||
// -- On xhr.open: capture initial metadata | ||
instrument(XMLHttpRequest.prototype, 'open', function(original) { | ||
return function() { | ||
const xhr = this; | ||
const url = arguments[1]; | ||
const method = typeof arguments[0] === 'string' ? arguments[0].toUpperCase() : arguments[0]; | ||
const message = `${method} ${localURLPathname(url)}`; | ||
this.__hb_xhr = { | ||
type: 'xhr', | ||
method, | ||
url, | ||
message, | ||
}; | ||
if (typeof original === 'function') { | ||
original.apply(xhr, arguments); | ||
} | ||
}; | ||
}); | ||
// -- On xhr.send: set up xhr.onreadystatechange to report breadcrumb | ||
instrument(XMLHttpRequest.prototype, 'send', function(original) { | ||
return function() { | ||
const xhr = this; | ||
function onreadystatechangeHandler() { | ||
if (xhr.readyState === 4) { | ||
let message; | ||
if (xhr.__hb_xhr) { | ||
xhr.__hb_xhr.status_code = xhr.status; | ||
message = xhr.__hb_xhr.message; | ||
delete xhr.__hb_xhr.message; | ||
} | ||
self.addBreadcrumb(message || 'XMLHttpRequest', { | ||
category: 'request', | ||
metadata: xhr.__hb_xhr, | ||
}); | ||
} | ||
} | ||
if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') { | ||
instrument(xhr, 'onreadystatechange', function(original) { | ||
return function() { | ||
onreadystatechangeHandler(); | ||
if (typeof original === 'function') { | ||
original.apply(this, arguments); | ||
} | ||
}; | ||
}); | ||
} else { | ||
xhr.onreadystatechange = onreadystatechangeHandler; | ||
} | ||
if (typeof original === 'function') { | ||
original.apply(xhr, arguments); | ||
} | ||
}; | ||
}); | ||
})(); | ||
// Breadcrumbs: instrument fetch | ||
(function() { | ||
if (!nativeFetch()) { | ||
// Polyfills use XHR. | ||
return; | ||
} | ||
}; | ||
instrument(window, 'setTimeout', instrumentTimer); | ||
instrument(window, 'setInterval', instrumentTimer); | ||
instrument(window, 'fetch', function(original) { | ||
return function() { | ||
const input = arguments[0]; | ||
let method = 'GET'; | ||
let url; | ||
if (typeof input === 'string') { | ||
url = input; | ||
} else if ('Request' in window && input instanceof Request) { | ||
url = input.url; | ||
if (input.method) { | ||
method = input.method; | ||
} | ||
} else { | ||
url = String(input); | ||
} | ||
if (arguments[1] && arguments[1].method) { | ||
method = arguments[1].method; | ||
} | ||
if (typeof method === 'string') { | ||
method = method.toUpperCase(); | ||
} | ||
const message = `${method} ${localURLPathname(url)}`; | ||
const metadata = { | ||
type: 'fetch', | ||
method, | ||
url, | ||
}; | ||
return original | ||
.apply(this, arguments) | ||
.then(function(response) { | ||
metadata.status_code = response.status; | ||
self.addBreadcrumb(message, { | ||
category: 'request', | ||
metadata, | ||
}); | ||
return response; | ||
}) | ||
.catch(function(error) { | ||
self.addBreadcrumb('fetch error', { | ||
category: 'error', | ||
metadata, | ||
}); | ||
throw error; | ||
}); | ||
}; | ||
}); | ||
})(); | ||
// Breadcrumbs: instrument navigation | ||
(function() { | ||
// The last known href of the current page | ||
let lastHref = window.location.href; | ||
function recordUrlChange(from, to) { | ||
lastHref = to; | ||
self.addBreadcrumb('Page changed', { | ||
category: 'navigation', | ||
metadata: { | ||
from, | ||
to, | ||
}, | ||
}); | ||
} | ||
// https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onpopstate | ||
instrument(window, 'onpopstate', function(original) { | ||
return function() { | ||
recordUrlChange(lastHref, window.location.href); | ||
if (original) { | ||
return original.apply(this, arguments); | ||
} | ||
}; | ||
}); | ||
// https://developer.mozilla.org/en-US/docs/Web/API/History/pushState | ||
// https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState | ||
function historyWrapper(original) { | ||
return function() { | ||
const url = arguments.length > 2 ? arguments[2] : undefined; | ||
if (url) { | ||
recordUrlChange(lastHref, String(url)); | ||
} | ||
return original.apply(this, arguments); | ||
}; | ||
} | ||
instrument(window.history, 'pushState', historyWrapper); | ||
instrument(window.history, 'replaceState', historyWrapper); | ||
})(); | ||
// Breadcrumbs: instrument console | ||
(function() { | ||
function inspectArray(obj) { | ||
if (!Array.isArray(obj)) { return ''; } | ||
return obj.map(value => { | ||
try { | ||
return String(value); | ||
} catch (e) { | ||
return '[unknown]'; | ||
} | ||
}).join(' '); | ||
} | ||
['debug', 'info', 'warn', 'error', 'log'].forEach(level => { | ||
instrument(window.console, level, function(original) { | ||
return function() { | ||
const args = Array.prototype.slice.call(arguments); | ||
const message = inspectArray(args); | ||
const opts = { | ||
category: 'log', | ||
metadata: { | ||
level: level, | ||
arguments: sanitize(args, 3), | ||
}, | ||
}; | ||
self.addBreadcrumb(message, opts); | ||
if (typeof original === 'function') { | ||
Function.prototype.apply.call(original, window.console, arguments); | ||
} | ||
}; | ||
}); | ||
}); | ||
})(); | ||
// Wrap timers | ||
(function() { | ||
function instrumentTimer(wrapOpts) { | ||
return 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, wrapOpts); | ||
return original(function() { | ||
func.apply(null, args); | ||
}, delay); | ||
} else { | ||
return original(func, delay); | ||
} | ||
}; | ||
}; | ||
} instrument(window, 'setTimeout', instrumentTimer({ component: 'setTimeout' })); | ||
instrument(window, 'setInterval', instrumentTimer({ component: 'setInterval' })); | ||
})(); | ||
// Wrap event listeners | ||
// Event targets borrowed from bugsnag-js: | ||
@@ -530,2 +997,4 @@ // See https://github.com/bugsnag/bugsnag-js/blob/d55af916a4d3c7757f979d887f9533fe1a04cc93/src/bugsnag.js#L542 | ||
instrument(prototype, 'addEventListener', function(original) { | ||
const wrapOpts = {component: `${prop}.prototype.addEventListener`}; | ||
// See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener | ||
@@ -535,3 +1004,3 @@ return function(type, listener, useCapture, wantsUntrusted) { | ||
if (listener && listener.handleEvent != null) { | ||
listener.handleEvent = wrap(listener.handleEvent); | ||
listener.handleEvent = wrap(listener.handleEvent, wrapOpts); | ||
} | ||
@@ -542,3 +1011,3 @@ } catch(e) { | ||
} | ||
return original.call(this, type, wrap(listener), useCapture, wantsUntrusted); | ||
return original.call(this, type, wrap(listener, wrapOpts), useCapture, wantsUntrusted); | ||
}; | ||
@@ -555,2 +1024,3 @@ }); | ||
// Wrap window.onerror | ||
instrument(window, 'onerror', function(original) { | ||
@@ -571,17 +1041,31 @@ function onerror(msg, url, line, col, err) { | ||
// simulate v8 stack | ||
var stack = [msg, '\n at ? (', url || 'unknown', ':', line || 0, ':', col || 0, ')'].join(''); | ||
// Simulate v8 stack | ||
const simulatedStack = [msg, '\n at ? (', url || 'unknown', ':', line || 0, ':', col || 0, ')'].join(''); | ||
let generated; | ||
if (err) { | ||
var generated = { stack: stackTrace(err) }; | ||
if (!generated.stack) { generated = {stack: stack}; } | ||
notify(err, generated); | ||
return; | ||
generated = { stack: stackTrace(err) }; | ||
if (!generated.stack) { generated = {stack: simulatedStack}; } | ||
} else { | ||
// Important: leave `generated` undefined | ||
err = { | ||
name: 'window.onerror', | ||
message: msg, | ||
stack: simulatedStack | ||
}; | ||
} | ||
notify({ | ||
name: 'window.onerror', | ||
message: msg, | ||
stack: stack | ||
}); | ||
self.addBreadcrumb( | ||
(err.name === 'window.onerror' || !err.name) ? 'window.onerror' : `window.onerror: ${err.name}`, | ||
{ | ||
category: 'error', | ||
metadata: { | ||
message: err.message, | ||
name: err.name, | ||
stack: generated ? generated.stack : err.stack | ||
} | ||
} | ||
); | ||
notify(err, generated); | ||
} | ||
@@ -598,2 +1082,3 @@ // See https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror | ||
// Wrap window.onunhandledrejection | ||
instrument(window, 'onunhandledrejection', function(original) { | ||
@@ -617,9 +1102,15 @@ // See https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event | ||
let stack = stackTrace(reason) || stackFallback; | ||
notify({ | ||
let err = { | ||
name: reason.name, | ||
message: `UnhandledPromiseRejectionWarning: ${reason}`, | ||
stack | ||
}); | ||
}; | ||
self.addBreadcrumb( | ||
`window.onunhandledrejection: ${err.name}`, | ||
{ | ||
category: 'error', | ||
metadata: err | ||
} | ||
); | ||
notify(err); | ||
return; | ||
@@ -640,3 +1131,3 @@ } | ||
} | ||
} | ||
}; | ||
}); | ||
@@ -643,0 +1134,0 @@ |
@@ -0,5 +1,203 @@ | ||
function sanitize(obj, maxDepth) { | ||
let seenObjects = []; | ||
function seen(obj) { | ||
if (!obj || typeof(obj) !== 'object') { return false; } | ||
for (let i = 0; i < seenObjects.length; i++) { | ||
const value = seenObjects[i]; | ||
if (value === obj) { | ||
return true; | ||
} | ||
} | ||
seenObjects.push(obj); | ||
return false; | ||
} | ||
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, depth) { | ||
if (!depth) { depth = 0; } | ||
if (depth >= maxDepth) { | ||
return '[DEPTH]'; | ||
} | ||
// Inspect invalid types | ||
if (!canSerialize(obj)) { return Object.prototype.toString.call(obj); } | ||
// Halt circular references | ||
if (seen(obj)) { | ||
return '[RECURSION]'; | ||
} | ||
// Serialize inside arrays | ||
if (Array.isArray(obj)) { | ||
return obj.map(o => serialize(o, depth+1)); | ||
} | ||
// Serialize inside objects | ||
if (typeof(obj) === 'object') { | ||
let ret = {}; | ||
for (const k in obj) { | ||
const v = obj[k]; | ||
if (Object.prototype.hasOwnProperty.call(obj, k) && (k != null) && (v != null)) { | ||
ret[k] = serialize(v, depth+1); | ||
} | ||
} | ||
return ret; | ||
} | ||
// Return everything else untouched | ||
return obj; | ||
} | ||
return serialize(obj); | ||
} | ||
/** | ||
* Converts an HTMLElement into a human-readable string. | ||
* @param {!HTMLElement} element | ||
* @return {string} | ||
*/ | ||
function stringNameOfElement(element) { | ||
if (!element || !element.tagName) { return ''; } | ||
let name = element.tagName.toLowerCase(); | ||
// Ignore the root <html> element in selectors and events. | ||
if (name === 'html') { return ''; } | ||
if (element.id) { | ||
name += `#${element.id}`; | ||
} | ||
const stringClassNames = element.getAttribute('class'); | ||
if (stringClassNames) { | ||
stringClassNames.split(/\s+/).forEach(className => { | ||
name += `.${className}`; | ||
}); | ||
} | ||
['alt', 'name', 'title', 'type'].forEach(attrName => { | ||
let attr = element.getAttribute(attrName); | ||
if (attr) { | ||
name += `[${attrName}="${attr}"]`; | ||
} | ||
}); | ||
const siblings = getSiblings(element); | ||
if (siblings.length > 1) { | ||
name += `:nth-child(${Array.prototype.indexOf.call(siblings, element) + 1})`; | ||
} | ||
return name; | ||
} | ||
function stringSelectorOfElement(element) { | ||
const name = stringNameOfElement(element); | ||
if (element.parentNode && element.parentNode.tagName) { | ||
const parentName = stringSelectorOfElement(element.parentNode); | ||
if (parentName.length > 0) { | ||
return `${parentName} > ${name}`; | ||
} | ||
} | ||
return name; | ||
} | ||
function stringTextOfElement(element) { | ||
return truncate((element.textContent || element.innerText || element.value || '').trim(), 300); | ||
} | ||
function nativeFetch() { | ||
if (!window.fetch) { return false; } | ||
if (isNative(window.fetch)) { return true; } | ||
// If fetch isn't native, it may be wrapped by someone else. Try to get | ||
// a pristine function from an iframe. | ||
try { | ||
const sandbox = document.createElement('iframe'); | ||
sandbox.style.display = 'none'; | ||
document.head.appendChild(sandbox); | ||
const result = sandbox.contentWindow.fetch && isNative(sandbox.contentWindow.fetch); | ||
document.head.removeChild(sandbox); | ||
return result; | ||
} catch(err) { | ||
if (console && console.warn) { | ||
console.warn('failed to detect native fetch via iframe: ' + err); | ||
} | ||
} | ||
return false; | ||
} | ||
function isNative(func) { | ||
return func.toString().indexOf('native') !== -1; | ||
} | ||
function parseURL(url) { | ||
// Regexp: https://tools.ietf.org/html/rfc3986#appendix-B | ||
const match = url.match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/) || {}; | ||
return { | ||
protocol: match[2], | ||
host: match[4], | ||
pathname: match[5], | ||
}; | ||
} | ||
function localURLPathname(url) { | ||
const parsed = parseURL(url); | ||
const parsedDocURL = parseURL(document.URL); | ||
// URL must be relative | ||
if (!parsed.host || parsed.protocol) { return parsed.pathname; } | ||
// Same domain | ||
if (parsed.protocol === parsedDocURL.protocol && parsed.host === parsedDocURL.host) { | ||
return parsed.pathname; | ||
} | ||
// x-domain | ||
return `${parsed.protocol}://${parsed.host}${parsed.pathname}`; | ||
} | ||
// Helpers | ||
function getSiblings(element) { | ||
try { | ||
const nodes = element.parentNode.childNodes; | ||
const siblings = []; | ||
Array.prototype.forEach.call(nodes, node => { | ||
if (node.tagName && node.tagName === element.tagName) { | ||
siblings.push(node); | ||
} | ||
}); | ||
return siblings; | ||
} catch(e) { | ||
return []; | ||
} | ||
} | ||
function truncate(string, length) { | ||
if (string.length > length) { | ||
string = string.substr(0, length) + '...'; | ||
} | ||
return string; | ||
} | ||
function builder() { | ||
var VERSION = '2.0.0', | ||
var VERSION = '2.1.0-beta.0', | ||
NOTIFIER = { | ||
name: 'honeybadger.js', | ||
name: 'honeybadger-js', | ||
url: 'https://github.com/honeybadger-io/honeybadger-js', | ||
@@ -79,3 +277,3 @@ version: VERSION, | ||
// access the stack property first. | ||
return err.stacktrace || err.stack || undefined | ||
return err.stacktrace || err.stack || undefined; | ||
} | ||
@@ -150,3 +348,4 @@ | ||
beforeNotifyHandlers: [], | ||
errorsSent: 0 | ||
breadcrumbs: [], | ||
errorsSent: 0, | ||
}; | ||
@@ -190,2 +389,6 @@ if (typeof opts === 'object') { | ||
function breadcrumbsEnabled() { | ||
return config('breadcrumbsEnabled', false); | ||
} | ||
function baseURL() { | ||
@@ -195,29 +398,2 @@ 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, depth) { | ||
var k, v, ret; | ||
ret = {}; | ||
if (!depth) { depth = 0; } | ||
if (depth >= config('max_depth', 8)) { | ||
return '[MAX DEPTH REACHED]'; | ||
} | ||
for (k in obj) { | ||
v = obj[k]; | ||
if (Object.prototype.hasOwnProperty.call(obj, k) && (k != null) && (v != null)) { | ||
if (!canSerialize(v)) { v = Object.prototype.toString.call(v); } | ||
ret[k] = (typeof v === 'object' ? serialize(v, depth+1) : v); | ||
} | ||
} | ||
return ret; | ||
} | ||
function request(apiKey, payload) { | ||
@@ -232,3 +408,3 @@ try { | ||
x.send(JSON.stringify(serialize(payload))); | ||
x.send(JSON.stringify(sanitize(payload, config('max_depth', 8)))); | ||
} catch(err) { | ||
@@ -305,2 +481,13 @@ log('Unable to send error report: error while initializing request', err, payload); | ||
self.addBreadcrumb('Honeybadger Notice', { | ||
category: 'notice', | ||
metadata: { | ||
message: err.message, | ||
name: err.name, | ||
stack: err.stack | ||
} | ||
}); | ||
err.breadcrumbs = self.breadcrumbs.slice(); | ||
let stack_before_handlers = err.stack; | ||
@@ -324,2 +511,6 @@ if (checkHandlers(self.beforeNotifyHandlers, err)) { return false; } | ||
'notifier': NOTIFIER, | ||
'breadcrumbs': { | ||
'enabled': breadcrumbsEnabled(), | ||
'trail': err.breadcrumbs, | ||
}, | ||
'error': { | ||
@@ -379,3 +570,4 @@ 'class': err.name, | ||
// removeEventListener. | ||
function wrap(fn, force) { | ||
function wrap(fn, opts) { | ||
if (!opts) { opts = {}; } | ||
try { | ||
@@ -389,8 +581,20 @@ if (typeof fn !== 'function') { return fn; } | ||
// object and there is a window.onerror handler available instead. | ||
if ((preferCatch && (onerror || force)) || (force && !onerror)) { | ||
if ((preferCatch && (onerror || opts.force)) || (opts.force && !onerror)) { | ||
try { | ||
return fn.apply(this, arguments); | ||
} catch (e) { | ||
notify(e); | ||
throw(e); | ||
} catch (err) { | ||
let generated = { stack: stackTrace(err) }; | ||
self.addBreadcrumb( | ||
opts.component ? `${opts.component}: ${err.name}` : err.name, | ||
{ | ||
category: 'error', | ||
metadata: { | ||
message: err.message, | ||
name: err.name, | ||
stack: generated.stack | ||
} | ||
} | ||
); | ||
notify(err, generated); | ||
throw(err); | ||
} | ||
@@ -439,3 +643,3 @@ } else { | ||
self.wrap = function(func) { | ||
return wrap(func, true); | ||
return wrap(func, { force: true }); | ||
}; | ||
@@ -475,2 +679,3 @@ | ||
self.beforeNotifyHandlers = []; | ||
self.breadcrumbs = []; | ||
for (var k in self) { | ||
@@ -493,2 +698,26 @@ if (indexOf.call(defaultProps, k) == -1) { | ||
self.addBreadcrumb = function(message, opts) { | ||
if (!breadcrumbsEnabled()) return; | ||
opts = opts || {}; | ||
const metadata = opts.metadata || undefined; | ||
const category = opts.category || 'custom'; | ||
const timestamp = new Date().toISOString(); | ||
self.breadcrumbs.push({ | ||
category: category, | ||
message: message, | ||
metadata: metadata || {}, | ||
timestamp: timestamp, | ||
}); | ||
const limit = config('maxBreadcrumbs', 40); | ||
if (self.breadcrumbs.length > limit) { | ||
self.breadcrumbs = self.breadcrumbs.slice(self.breadcrumbs.length - limit); | ||
} | ||
return self; | ||
}; | ||
// Install instrumentation. | ||
@@ -498,3 +727,3 @@ // This should happen once for the first factory call. | ||
if (notSingleton) { return; } | ||
if (!object || !name || !replacement) { return; } | ||
if (!object || !name || !replacement || !(name in object)) { return; } | ||
var original = object[name]; | ||
@@ -504,19 +733,257 @@ 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); | ||
// Breadcrumbs: instrument click events | ||
(function() { | ||
window.addEventListener('click', (event) => { | ||
let message, selector, text; | ||
try { | ||
message = stringNameOfElement(event.target); | ||
selector = stringSelectorOfElement(event.target); | ||
text = stringTextOfElement(event.target); | ||
} catch(e) { | ||
message = 'UI Click'; | ||
selector = '[unknown]'; | ||
text = '[unknown]'; | ||
} | ||
// There's nothing to display | ||
if (message.length === 0) { return; } | ||
self.addBreadcrumb(message, { | ||
category: 'ui.click', | ||
metadata: { | ||
selector, | ||
text, | ||
event, | ||
}, | ||
}); | ||
}, true); | ||
})(); | ||
// Breadcrumbs: instrument XMLHttpRequest | ||
(function() { | ||
// -- On xhr.open: capture initial metadata | ||
instrument(XMLHttpRequest.prototype, 'open', function(original) { | ||
return function() { | ||
const xhr = this; | ||
const url = arguments[1]; | ||
const method = typeof arguments[0] === 'string' ? arguments[0].toUpperCase() : arguments[0]; | ||
const message = `${method} ${localURLPathname(url)}`; | ||
this.__hb_xhr = { | ||
type: 'xhr', | ||
method, | ||
url, | ||
message, | ||
}; | ||
if (typeof original === 'function') { | ||
original.apply(xhr, arguments); | ||
} | ||
}; | ||
}); | ||
// -- On xhr.send: set up xhr.onreadystatechange to report breadcrumb | ||
instrument(XMLHttpRequest.prototype, 'send', function(original) { | ||
return function() { | ||
const xhr = this; | ||
function onreadystatechangeHandler() { | ||
if (xhr.readyState === 4) { | ||
let message; | ||
if (xhr.__hb_xhr) { | ||
xhr.__hb_xhr.status_code = xhr.status; | ||
message = xhr.__hb_xhr.message; | ||
delete xhr.__hb_xhr.message; | ||
} | ||
self.addBreadcrumb(message || 'XMLHttpRequest', { | ||
category: 'request', | ||
metadata: xhr.__hb_xhr, | ||
}); | ||
} | ||
} | ||
if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') { | ||
instrument(xhr, 'onreadystatechange', function(original) { | ||
return function() { | ||
onreadystatechangeHandler(); | ||
if (typeof original === 'function') { | ||
original.apply(this, arguments); | ||
} | ||
}; | ||
}); | ||
} else { | ||
xhr.onreadystatechange = onreadystatechangeHandler; | ||
} | ||
if (typeof original === 'function') { | ||
original.apply(xhr, arguments); | ||
} | ||
}; | ||
}); | ||
})(); | ||
// Breadcrumbs: instrument fetch | ||
(function() { | ||
if (!nativeFetch()) { | ||
// Polyfills use XHR. | ||
return; | ||
} | ||
}; | ||
instrument(window, 'setTimeout', instrumentTimer); | ||
instrument(window, 'setInterval', instrumentTimer); | ||
instrument(window, 'fetch', function(original) { | ||
return function() { | ||
const input = arguments[0]; | ||
let method = 'GET'; | ||
let url; | ||
if (typeof input === 'string') { | ||
url = input; | ||
} else if ('Request' in window && input instanceof Request) { | ||
url = input.url; | ||
if (input.method) { | ||
method = input.method; | ||
} | ||
} else { | ||
url = String(input); | ||
} | ||
if (arguments[1] && arguments[1].method) { | ||
method = arguments[1].method; | ||
} | ||
if (typeof method === 'string') { | ||
method = method.toUpperCase(); | ||
} | ||
const message = `${method} ${localURLPathname(url)}`; | ||
const metadata = { | ||
type: 'fetch', | ||
method, | ||
url, | ||
}; | ||
return original | ||
.apply(this, arguments) | ||
.then(function(response) { | ||
metadata.status_code = response.status; | ||
self.addBreadcrumb(message, { | ||
category: 'request', | ||
metadata, | ||
}); | ||
return response; | ||
}) | ||
.catch(function(error) { | ||
self.addBreadcrumb('fetch error', { | ||
category: 'error', | ||
metadata, | ||
}); | ||
throw error; | ||
}); | ||
}; | ||
}); | ||
})(); | ||
// Breadcrumbs: instrument navigation | ||
(function() { | ||
// The last known href of the current page | ||
let lastHref = window.location.href; | ||
function recordUrlChange(from, to) { | ||
lastHref = to; | ||
self.addBreadcrumb('Page changed', { | ||
category: 'navigation', | ||
metadata: { | ||
from, | ||
to, | ||
}, | ||
}); | ||
} | ||
// https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onpopstate | ||
instrument(window, 'onpopstate', function(original) { | ||
return function() { | ||
recordUrlChange(lastHref, window.location.href); | ||
if (original) { | ||
return original.apply(this, arguments); | ||
} | ||
}; | ||
}); | ||
// https://developer.mozilla.org/en-US/docs/Web/API/History/pushState | ||
// https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState | ||
function historyWrapper(original) { | ||
return function() { | ||
const url = arguments.length > 2 ? arguments[2] : undefined; | ||
if (url) { | ||
recordUrlChange(lastHref, String(url)); | ||
} | ||
return original.apply(this, arguments); | ||
}; | ||
} | ||
instrument(window.history, 'pushState', historyWrapper); | ||
instrument(window.history, 'replaceState', historyWrapper); | ||
})(); | ||
// Breadcrumbs: instrument console | ||
(function() { | ||
function inspectArray(obj) { | ||
if (!Array.isArray(obj)) { return ''; } | ||
return obj.map(value => { | ||
try { | ||
return String(value); | ||
} catch (e) { | ||
return '[unknown]'; | ||
} | ||
}).join(' '); | ||
} | ||
['debug', 'info', 'warn', 'error', 'log'].forEach(level => { | ||
instrument(window.console, level, function(original) { | ||
return function() { | ||
const args = Array.prototype.slice.call(arguments); | ||
const message = inspectArray(args); | ||
const opts = { | ||
category: 'log', | ||
metadata: { | ||
level: level, | ||
arguments: sanitize(args, 3), | ||
}, | ||
}; | ||
self.addBreadcrumb(message, opts); | ||
if (typeof original === 'function') { | ||
Function.prototype.apply.call(original, window.console, arguments); | ||
} | ||
}; | ||
}); | ||
}); | ||
})(); | ||
// Wrap timers | ||
(function() { | ||
function instrumentTimer(wrapOpts) { | ||
return 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, wrapOpts); | ||
return original(function() { | ||
func.apply(null, args); | ||
}, delay); | ||
} else { | ||
return original(func, delay); | ||
} | ||
}; | ||
}; | ||
} instrument(window, 'setTimeout', instrumentTimer({ component: 'setTimeout' })); | ||
instrument(window, 'setInterval', instrumentTimer({ component: 'setInterval' })); | ||
})(); | ||
// Wrap event listeners | ||
// Event targets borrowed from bugsnag-js: | ||
@@ -528,2 +995,4 @@ // See https://github.com/bugsnag/bugsnag-js/blob/d55af916a4d3c7757f979d887f9533fe1a04cc93/src/bugsnag.js#L542 | ||
instrument(prototype, 'addEventListener', function(original) { | ||
const wrapOpts = {component: `${prop}.prototype.addEventListener`}; | ||
// See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener | ||
@@ -533,3 +1002,3 @@ return function(type, listener, useCapture, wantsUntrusted) { | ||
if (listener && listener.handleEvent != null) { | ||
listener.handleEvent = wrap(listener.handleEvent); | ||
listener.handleEvent = wrap(listener.handleEvent, wrapOpts); | ||
} | ||
@@ -540,3 +1009,3 @@ } catch(e) { | ||
} | ||
return original.call(this, type, wrap(listener), useCapture, wantsUntrusted); | ||
return original.call(this, type, wrap(listener, wrapOpts), useCapture, wantsUntrusted); | ||
}; | ||
@@ -553,2 +1022,3 @@ }); | ||
// Wrap window.onerror | ||
instrument(window, 'onerror', function(original) { | ||
@@ -569,17 +1039,31 @@ function onerror(msg, url, line, col, err) { | ||
// simulate v8 stack | ||
var stack = [msg, '\n at ? (', url || 'unknown', ':', line || 0, ':', col || 0, ')'].join(''); | ||
// Simulate v8 stack | ||
const simulatedStack = [msg, '\n at ? (', url || 'unknown', ':', line || 0, ':', col || 0, ')'].join(''); | ||
let generated; | ||
if (err) { | ||
var generated = { stack: stackTrace(err) }; | ||
if (!generated.stack) { generated = {stack: stack}; } | ||
notify(err, generated); | ||
return; | ||
generated = { stack: stackTrace(err) }; | ||
if (!generated.stack) { generated = {stack: simulatedStack}; } | ||
} else { | ||
// Important: leave `generated` undefined | ||
err = { | ||
name: 'window.onerror', | ||
message: msg, | ||
stack: simulatedStack | ||
}; | ||
} | ||
notify({ | ||
name: 'window.onerror', | ||
message: msg, | ||
stack: stack | ||
}); | ||
self.addBreadcrumb( | ||
(err.name === 'window.onerror' || !err.name) ? 'window.onerror' : `window.onerror: ${err.name}`, | ||
{ | ||
category: 'error', | ||
metadata: { | ||
message: err.message, | ||
name: err.name, | ||
stack: generated ? generated.stack : err.stack | ||
} | ||
} | ||
); | ||
notify(err, generated); | ||
} | ||
@@ -596,2 +1080,3 @@ // See https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror | ||
// Wrap window.onunhandledrejection | ||
instrument(window, 'onunhandledrejection', function(original) { | ||
@@ -615,9 +1100,15 @@ // See https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event | ||
let stack = stackTrace(reason) || stackFallback; | ||
notify({ | ||
let err = { | ||
name: reason.name, | ||
message: `UnhandledPromiseRejectionWarning: ${reason}`, | ||
stack | ||
}); | ||
}; | ||
self.addBreadcrumb( | ||
`window.onunhandledrejection: ${err.name}`, | ||
{ | ||
category: 'error', | ||
metadata: err | ||
} | ||
); | ||
notify(err); | ||
return; | ||
@@ -638,3 +1129,3 @@ } | ||
} | ||
} | ||
}; | ||
}); | ||
@@ -641,0 +1132,0 @@ |
@@ -21,6 +21,227 @@ (function (global, factory) { | ||
function sanitize(obj, maxDepth) { | ||
var seenObjects = []; | ||
function seen(obj) { | ||
if (!obj || _typeof(obj) !== 'object') { | ||
return false; | ||
} | ||
for (var i = 0; i < seenObjects.length; i++) { | ||
var value = seenObjects[i]; | ||
if (value === obj) { | ||
return true; | ||
} | ||
} | ||
seenObjects.push(obj); | ||
return false; | ||
} | ||
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, depth) { | ||
if (!depth) { | ||
depth = 0; | ||
} | ||
if (depth >= maxDepth) { | ||
return '[DEPTH]'; | ||
} // Inspect invalid types | ||
if (!canSerialize(obj)) { | ||
return Object.prototype.toString.call(obj); | ||
} // Halt circular references | ||
if (seen(obj)) { | ||
return '[RECURSION]'; | ||
} // Serialize inside arrays | ||
if (Array.isArray(obj)) { | ||
return obj.map(function (o) { | ||
return serialize(o, depth + 1); | ||
}); | ||
} // Serialize inside objects | ||
if (_typeof(obj) === 'object') { | ||
var ret = {}; | ||
for (var k in obj) { | ||
var v = obj[k]; | ||
if (Object.prototype.hasOwnProperty.call(obj, k) && k != null && v != null) { | ||
ret[k] = serialize(v, depth + 1); | ||
} | ||
} | ||
return ret; | ||
} // Return everything else untouched | ||
return obj; | ||
} | ||
return serialize(obj); | ||
} | ||
/** | ||
* Converts an HTMLElement into a human-readable string. | ||
* @param {!HTMLElement} element | ||
* @return {string} | ||
*/ | ||
function stringNameOfElement(element) { | ||
if (!element || !element.tagName) { | ||
return ''; | ||
} | ||
var name = element.tagName.toLowerCase(); // Ignore the root <html> element in selectors and events. | ||
if (name === 'html') { | ||
return ''; | ||
} | ||
if (element.id) { | ||
name += "#".concat(element.id); | ||
} | ||
var stringClassNames = element.getAttribute('class'); | ||
if (stringClassNames) { | ||
stringClassNames.split(/\s+/).forEach(function (className) { | ||
name += ".".concat(className); | ||
}); | ||
} | ||
['alt', 'name', 'title', 'type'].forEach(function (attrName) { | ||
var attr = element.getAttribute(attrName); | ||
if (attr) { | ||
name += "[".concat(attrName, "=\"").concat(attr, "\"]"); | ||
} | ||
}); | ||
var siblings = getSiblings(element); | ||
if (siblings.length > 1) { | ||
name += ":nth-child(".concat(Array.prototype.indexOf.call(siblings, element) + 1, ")"); | ||
} | ||
return name; | ||
} | ||
function stringSelectorOfElement(element) { | ||
var name = stringNameOfElement(element); | ||
if (element.parentNode && element.parentNode.tagName) { | ||
var parentName = stringSelectorOfElement(element.parentNode); | ||
if (parentName.length > 0) { | ||
return "".concat(parentName, " > ").concat(name); | ||
} | ||
} | ||
return name; | ||
} | ||
function stringTextOfElement(element) { | ||
return truncate((element.textContent || element.innerText || element.value || '').trim(), 300); | ||
} | ||
function nativeFetch() { | ||
if (!window.fetch) { | ||
return false; | ||
} | ||
if (isNative(window.fetch)) { | ||
return true; | ||
} // If fetch isn't native, it may be wrapped by someone else. Try to get | ||
// a pristine function from an iframe. | ||
try { | ||
var sandbox = document.createElement('iframe'); | ||
sandbox.style.display = 'none'; | ||
document.head.appendChild(sandbox); | ||
var result = sandbox.contentWindow.fetch && isNative(sandbox.contentWindow.fetch); | ||
document.head.removeChild(sandbox); | ||
return result; | ||
} catch (err) { | ||
if (console && console.warn) { | ||
console.warn('failed to detect native fetch via iframe: ' + err); | ||
} | ||
} | ||
return false; | ||
} | ||
function isNative(func) { | ||
return func.toString().indexOf('native') !== -1; | ||
} | ||
function parseURL(url) { | ||
// Regexp: https://tools.ietf.org/html/rfc3986#appendix-B | ||
var match = url.match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/) || {}; | ||
return { | ||
protocol: match[2], | ||
host: match[4], | ||
pathname: match[5] | ||
}; | ||
} | ||
function localURLPathname(url) { | ||
var parsed = parseURL(url); | ||
var parsedDocURL = parseURL(document.URL); // URL must be relative | ||
if (!parsed.host || parsed.protocol) { | ||
return parsed.pathname; | ||
} // Same domain | ||
if (parsed.protocol === parsedDocURL.protocol && parsed.host === parsedDocURL.host) { | ||
return parsed.pathname; | ||
} // x-domain | ||
return "".concat(parsed.protocol, "://").concat(parsed.host).concat(parsed.pathname); | ||
} // Helpers | ||
function getSiblings(element) { | ||
try { | ||
var nodes = element.parentNode.childNodes; | ||
var siblings = []; | ||
Array.prototype.forEach.call(nodes, function (node) { | ||
if (node.tagName && node.tagName === element.tagName) { | ||
siblings.push(node); | ||
} | ||
}); | ||
return siblings; | ||
} catch (e) { | ||
return []; | ||
} | ||
} | ||
function truncate(string, length) { | ||
if (string.length > length) { | ||
string = string.substr(0, length) + '...'; | ||
} | ||
return string; | ||
} | ||
function builder() { | ||
var VERSION = '2.0.0', | ||
var VERSION = '2.1.0-beta.0', | ||
NOTIFIER = { | ||
name: 'honeybadger.js', | ||
name: 'honeybadger-js', | ||
url: 'https://github.com/honeybadger-io/honeybadger-js', | ||
@@ -208,2 +429,3 @@ version: VERSION, | ||
beforeNotifyHandlers: [], | ||
breadcrumbs: [], | ||
errorsSent: 0 | ||
@@ -268,2 +490,6 @@ }; | ||
function breadcrumbsEnabled() { | ||
return config('breadcrumbsEnabled', false); | ||
} | ||
function baseURL() { | ||
@@ -273,43 +499,2 @@ 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, depth) { | ||
var k, v, ret; | ||
ret = {}; | ||
if (!depth) { | ||
depth = 0; | ||
} | ||
if (depth >= config('max_depth', 8)) { | ||
return '[MAX DEPTH REACHED]'; | ||
} | ||
for (k in obj) { | ||
v = obj[k]; | ||
if (Object.prototype.hasOwnProperty.call(obj, k) && k != null && v != null) { | ||
if (!canSerialize(v)) { | ||
v = Object.prototype.toString.call(v); | ||
} | ||
ret[k] = _typeof(v) === 'object' ? serialize(v, depth + 1) : v; | ||
} | ||
} | ||
return ret; | ||
} | ||
function request(apiKey, payload) { | ||
@@ -322,3 +507,3 @@ try { | ||
x.setRequestHeader('Accept', 'text/json, application/json'); | ||
x.send(JSON.stringify(serialize(payload))); | ||
x.send(JSON.stringify(sanitize(payload, config('max_depth', 8)))); | ||
} catch (err) { | ||
@@ -404,2 +589,11 @@ log('Unable to send error report: error while initializing request', err, payload); | ||
}); | ||
self.addBreadcrumb('Honeybadger Notice', { | ||
category: 'notice', | ||
metadata: { | ||
message: err.message, | ||
name: err.name, | ||
stack: err.stack | ||
} | ||
}); | ||
err.breadcrumbs = self.breadcrumbs.slice(); | ||
var stack_before_handlers = err.stack; | ||
@@ -430,2 +624,6 @@ | ||
'notifier': NOTIFIER, | ||
'breadcrumbs': { | ||
'enabled': breadcrumbsEnabled(), | ||
'trail': err.breadcrumbs | ||
}, | ||
'error': { | ||
@@ -487,3 +685,7 @@ 'class': err.name, | ||
function wrap(fn, force) { | ||
function wrap(fn, opts) { | ||
if (!opts) { | ||
opts = {}; | ||
} | ||
try { | ||
@@ -503,8 +705,19 @@ if (typeof fn !== 'function') { | ||
if (preferCatch && (onerror || force) || force && !onerror) { | ||
if (preferCatch && (onerror || opts.force) || opts.force && !onerror) { | ||
try { | ||
return fn.apply(this, arguments); | ||
} catch (e) { | ||
notify(e); | ||
throw e; | ||
} catch (err) { | ||
var generated = { | ||
stack: stackTrace(err) | ||
}; | ||
self.addBreadcrumb(opts.component ? "".concat(opts.component, ": ").concat(err.name) : err.name, { | ||
category: 'error', | ||
metadata: { | ||
message: err.message, | ||
name: err.name, | ||
stack: generated.stack | ||
} | ||
}); | ||
notify(err, generated); | ||
throw err; | ||
} | ||
@@ -565,3 +778,5 @@ } else { | ||
self.wrap = function (func) { | ||
return wrap(func, true); | ||
return wrap(func, { | ||
force: true | ||
}); | ||
}; | ||
@@ -611,2 +826,3 @@ | ||
self.beforeNotifyHandlers = []; | ||
self.breadcrumbs = []; | ||
@@ -629,2 +845,23 @@ for (var k in self) { | ||
return VERSION; | ||
}; | ||
self.addBreadcrumb = function (message, opts) { | ||
if (!breadcrumbsEnabled()) return; | ||
opts = opts || {}; | ||
var metadata = opts.metadata || undefined; | ||
var category = opts.category || 'custom'; | ||
var timestamp = new Date().toISOString(); | ||
self.breadcrumbs.push({ | ||
category: category, | ||
message: message, | ||
metadata: metadata || {}, | ||
timestamp: timestamp | ||
}); | ||
var limit = config('maxBreadcrumbs', 40); | ||
if (self.breadcrumbs.length > limit) { | ||
self.breadcrumbs = self.breadcrumbs.slice(self.breadcrumbs.length - limit); | ||
} | ||
return self; | ||
}; // Install instrumentation. | ||
@@ -639,3 +876,3 @@ // This should happen once for the first factory call. | ||
if (!object || !name || !replacement) { | ||
if (!object || !name || !replacement || !(name in object)) { | ||
return; | ||
@@ -646,23 +883,266 @@ } | ||
object[name] = replacement(original); | ||
} | ||
} // Breadcrumbs: instrument click events | ||
var instrumentTimer = function instrumentTimer(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); | ||
(function () { | ||
window.addEventListener('click', function (event) { | ||
var message, selector, text; | ||
try { | ||
message = stringNameOfElement(event.target); | ||
selector = stringSelectorOfElement(event.target); | ||
text = stringTextOfElement(event.target); | ||
} catch (e) { | ||
message = 'UI Click'; | ||
selector = '[unknown]'; | ||
text = '[unknown]'; | ||
} // There's nothing to display | ||
if (message.length === 0) { | ||
return; | ||
} | ||
}; | ||
}; | ||
self.addBreadcrumb(message, { | ||
category: 'ui.click', | ||
metadata: { | ||
selector: selector, | ||
text: text, | ||
event: event | ||
} | ||
}); | ||
}, true); | ||
})(); // Breadcrumbs: instrument XMLHttpRequest | ||
instrument(window, 'setTimeout', instrumentTimer); | ||
instrument(window, 'setInterval', instrumentTimer); // Event targets borrowed from bugsnag-js: | ||
(function () { | ||
// -- On xhr.open: capture initial metadata | ||
instrument(XMLHttpRequest.prototype, 'open', function (original) { | ||
return function () { | ||
var xhr = this; | ||
var url = arguments[1]; | ||
var method = typeof arguments[0] === 'string' ? arguments[0].toUpperCase() : arguments[0]; | ||
var message = "".concat(method, " ").concat(localURLPathname(url)); | ||
this.__hb_xhr = { | ||
type: 'xhr', | ||
method: method, | ||
url: url, | ||
message: message | ||
}; | ||
if (typeof original === 'function') { | ||
original.apply(xhr, arguments); | ||
} | ||
}; | ||
}); // -- On xhr.send: set up xhr.onreadystatechange to report breadcrumb | ||
instrument(XMLHttpRequest.prototype, 'send', function (original) { | ||
return function () { | ||
var xhr = this; | ||
function onreadystatechangeHandler() { | ||
if (xhr.readyState === 4) { | ||
var message; | ||
if (xhr.__hb_xhr) { | ||
xhr.__hb_xhr.status_code = xhr.status; | ||
message = xhr.__hb_xhr.message; | ||
delete xhr.__hb_xhr.message; | ||
} | ||
self.addBreadcrumb(message || 'XMLHttpRequest', { | ||
category: 'request', | ||
metadata: xhr.__hb_xhr | ||
}); | ||
} | ||
} | ||
if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') { | ||
instrument(xhr, 'onreadystatechange', function (original) { | ||
return function () { | ||
onreadystatechangeHandler(); | ||
if (typeof original === 'function') { | ||
original.apply(this, arguments); | ||
} | ||
}; | ||
}); | ||
} else { | ||
xhr.onreadystatechange = onreadystatechangeHandler; | ||
} | ||
if (typeof original === 'function') { | ||
original.apply(xhr, arguments); | ||
} | ||
}; | ||
}); | ||
})(); // Breadcrumbs: instrument fetch | ||
(function () { | ||
if (!nativeFetch()) { | ||
// Polyfills use XHR. | ||
return; | ||
} | ||
instrument(window, 'fetch', function (original) { | ||
return function () { | ||
var input = arguments[0]; | ||
var method = 'GET'; | ||
var url; | ||
if (typeof input === 'string') { | ||
url = input; | ||
} else if ('Request' in window && input instanceof Request) { | ||
url = input.url; | ||
if (input.method) { | ||
method = input.method; | ||
} | ||
} else { | ||
url = String(input); | ||
} | ||
if (arguments[1] && arguments[1].method) { | ||
method = arguments[1].method; | ||
} | ||
if (typeof method === 'string') { | ||
method = method.toUpperCase(); | ||
} | ||
var message = "".concat(method, " ").concat(localURLPathname(url)); | ||
var metadata = { | ||
type: 'fetch', | ||
method: method, | ||
url: url | ||
}; | ||
return original.apply(this, arguments).then(function (response) { | ||
metadata.status_code = response.status; | ||
self.addBreadcrumb(message, { | ||
category: 'request', | ||
metadata: metadata | ||
}); | ||
return response; | ||
}).catch(function (error) { | ||
self.addBreadcrumb('fetch error', { | ||
category: 'error', | ||
metadata: metadata | ||
}); | ||
throw error; | ||
}); | ||
}; | ||
}); | ||
})(); // Breadcrumbs: instrument navigation | ||
(function () { | ||
// The last known href of the current page | ||
var lastHref = window.location.href; | ||
function recordUrlChange(from, to) { | ||
lastHref = to; | ||
self.addBreadcrumb('Page changed', { | ||
category: 'navigation', | ||
metadata: { | ||
from: from, | ||
to: to | ||
} | ||
}); | ||
} // https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onpopstate | ||
instrument(window, 'onpopstate', function (original) { | ||
return function () { | ||
recordUrlChange(lastHref, window.location.href); | ||
if (original) { | ||
return original.apply(this, arguments); | ||
} | ||
}; | ||
}); // https://developer.mozilla.org/en-US/docs/Web/API/History/pushState | ||
// https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState | ||
function historyWrapper(original) { | ||
return function () { | ||
var url = arguments.length > 2 ? arguments[2] : undefined; | ||
if (url) { | ||
recordUrlChange(lastHref, String(url)); | ||
} | ||
return original.apply(this, arguments); | ||
}; | ||
} | ||
instrument(window.history, 'pushState', historyWrapper); | ||
instrument(window.history, 'replaceState', historyWrapper); | ||
})(); // Breadcrumbs: instrument console | ||
(function () { | ||
function inspectArray(obj) { | ||
if (!Array.isArray(obj)) { | ||
return ''; | ||
} | ||
return obj.map(function (value) { | ||
try { | ||
return String(value); | ||
} catch (e) { | ||
return '[unknown]'; | ||
} | ||
}).join(' '); | ||
} | ||
['debug', 'info', 'warn', 'error', 'log'].forEach(function (level) { | ||
instrument(window.console, level, function (original) { | ||
return function () { | ||
var args = Array.prototype.slice.call(arguments); | ||
var message = inspectArray(args); | ||
var opts = { | ||
category: 'log', | ||
metadata: { | ||
level: level, | ||
arguments: sanitize(args, 3) | ||
} | ||
}; | ||
self.addBreadcrumb(message, opts); | ||
if (typeof original === 'function') { | ||
Function.prototype.apply.call(original, window.console, arguments); | ||
} | ||
}; | ||
}); | ||
}); | ||
})(); // Wrap timers | ||
(function () { | ||
function instrumentTimer(wrapOpts) { | ||
return 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, wrapOpts); | ||
return original(function () { | ||
func.apply(null, args); | ||
}, delay); | ||
} else { | ||
return original(func, delay); | ||
} | ||
}; | ||
}; | ||
} | ||
instrument(window, 'setTimeout', instrumentTimer({ | ||
component: 'setTimeout' | ||
})); | ||
instrument(window, 'setInterval', instrumentTimer({ | ||
component: 'setInterval' | ||
})); | ||
})(); // Wrap event listeners | ||
// 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) { | ||
@@ -673,7 +1153,10 @@ var prototype = window[prop] && window[prop].prototype; | ||
instrument(prototype, 'addEventListener', function (original) { | ||
// See https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener | ||
var wrapOpts = { | ||
component: "".concat(prop, ".prototype.addEventListener") | ||
}; // 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); | ||
listener.handleEvent = wrap(listener.handleEvent, wrapOpts); | ||
} | ||
@@ -685,3 +1168,3 @@ } catch (e) { | ||
return original.call(this, type, wrap(listener), useCapture, wantsUntrusted); | ||
return original.call(this, type, wrap(listener, wrapOpts), useCapture, wantsUntrusted); | ||
}; | ||
@@ -696,3 +1179,4 @@ }); | ||
} | ||
}); | ||
}); // Wrap window.onerror | ||
instrument(window, 'onerror', function (original) { | ||
@@ -714,9 +1198,10 @@ function onerror(msg, url, line, col, err) { | ||
return; | ||
} // simulate v8 stack | ||
} // Simulate v8 stack | ||
var stack = [msg, '\n at ? (', url || 'unknown', ':', line || 0, ':', col || 0, ')'].join(''); | ||
var simulatedStack = [msg, '\n at ? (', url || 'unknown', ':', line || 0, ':', col || 0, ')'].join(''); | ||
var generated; | ||
if (err) { | ||
var generated = { | ||
generated = { | ||
stack: stackTrace(err) | ||
@@ -727,15 +1212,23 @@ }; | ||
generated = { | ||
stack: stack | ||
stack: simulatedStack | ||
}; | ||
} | ||
notify(err, generated); | ||
return; | ||
} else { | ||
// Important: leave `generated` undefined | ||
err = { | ||
name: 'window.onerror', | ||
message: msg, | ||
stack: simulatedStack | ||
}; | ||
} | ||
notify({ | ||
name: 'window.onerror', | ||
message: msg, | ||
stack: stack | ||
self.addBreadcrumb(err.name === 'window.onerror' || !err.name ? 'window.onerror' : "window.onerror: ".concat(err.name), { | ||
category: 'error', | ||
metadata: { | ||
message: err.message, | ||
name: err.name, | ||
stack: generated ? generated.stack : err.stack | ||
} | ||
}); | ||
notify(err, generated); | ||
} // See https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror | ||
@@ -753,3 +1246,4 @@ | ||
}; | ||
}); | ||
}); // Wrap window.onunhandledrejection | ||
instrument(window, 'onunhandledrejection', function (original) { | ||
@@ -776,7 +1270,12 @@ // See https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event | ||
var stack = stackTrace(reason) || stackFallback; | ||
notify({ | ||
var err = { | ||
name: reason.name, | ||
message: "UnhandledPromiseRejectionWarning: ".concat(reason), | ||
stack: stack | ||
}; | ||
self.addBreadcrumb("window.onunhandledrejection: ".concat(err.name), { | ||
category: 'error', | ||
metadata: err | ||
}); | ||
notify(err); | ||
return; | ||
@@ -783,0 +1282,0 @@ } |
@@ -1,2 +0,2 @@ | ||
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e=e||self).Honeybadger=n()}(this,function(){"use strict";function R(e){return(R="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}var e=function(){var b,w,j="2.0.0",k={name:"honeybadger.js",url:"https://github.com/honeybadger-io/honeybadger-js",version:j,language:"javascript"},E=!1,x=!1;function _(e,n){var t={};for(var r in e)t[r]=e[r];for(var r in n)t[r]=n[r];return t}function S(e,n){var t=_(e,n);return e.context&&n.context&&(t.context=_(e.context,n.context)),t}function T(e){return!!b&&b.name===e.name&&b.message===e.message&&b.stack===e.stack}function O(e){return e.stacktrace||e.stack||void 0}return function(e){var c=x,n=[],u=[],f={context:{},beforeNotifyHandlers:[],errorsSent:0};if("object"===R(e))for(var t in e)f[t]=e[t];function s(){var e=window.console;if(e){var n=Array.prototype.slice.call(arguments);n.unshift("[Honeybadger]"),e.log.apply(e,n)}}function l(){if(p("debug"))return s.apply(this,arguments)}function p(e,n){var t=f[e];return void 0===t&&(t=f[e.toLowerCase()]),"false"===t&&(t=!1),void 0!==t?t:n}function d(){return!c&&p("onerror",!0)}function r(e,n){try{var t=new XMLHttpRequest;t.open("POST","http"+(p("ssl",!0)?"s":"")+"://"+p("host","api.honeybadger.io")+"/v1/notices/js",p("async",!0)),t.setRequestHeader("X-API-Key",e),t.setRequestHeader("Content-Type","application/json"),t.setRequestHeader("Accept","text/json, application/json"),t.send(JSON.stringify(function e(n,t){var r,o,i,a;if(i={},t||(t=0),t>=p("max_depth",8))return"[MAX DEPTH REACHED]";for(r in n)o=n[r],Object.prototype.hasOwnProperty.call(n,r)&&null!=r&&null!=o&&((/function|symbol/.test(R(a=o))||"object"===R(a)&&void 0===a.hasOwnProperty)&&(o=Object.prototype.toString.call(o)),i[r]="object"===R(o)?e(o,t+1):o);return i}(n)))}catch(e){s("Unable to send error report: error while initializing request",e,n)}}function v(e){if(b=w=null,p("disabled",!1))return l("Dropping notice: honeybadger.js is disabled",e),!1;var n,t=p("apiKey",p("api_key"));return t?(n=p("maxErrors"))&&f.errorsSent>=n?(l("Dropping notice: max errors exceeded",e),!1):(f.errorsSent++,r(t,e),!0):(s("Unable to send error report: no API key has been configured"),!1)}function y(e,n){if(e||(e={}),"[object Error]"===Object.prototype.toString.call(e)){var t=e;e=_(e,{name:t.name,message:t.message,stack:O(t)})}if("object"!==R(e)){var r=String(e);e={message:r}}if(T(e))return!1;if(w&&E&&v(w),function(e){for(var n in e)if(Object.prototype.hasOwnProperty.call(e,n))return!1;return!0}(e))return!1;var o;n&&(e.stack=n.stack,o=n.generator);var i=(e=_(e,{name:e.name||"Error",context:_(f.context,e.context),url:e.url||document.URL,projectRoot:e.projectRoot||e.project_root||p("projectRoot",p("project_root",window.location.protocol+"//"+window.location.host)),environment:e.environment||p("environment"),component:e.component||p("component"),action:e.action||p("action"),revision:e.revision||p("revision")})).stack;if(function(e,n){var t,r;for(t=0,r=e.length;t<r;t++)if(!1===(0,e[t])(n))return!0;return!1}(f.beforeNotifyHandlers,e))return!1;if(e.stack!=i&&(o=void 0),function(e,n){var t=e.message;for(var r in n)if(t.match(n[r]))return!0;return!1}(e,p("ignorePatterns")))return!1;var a,c=((a={}).HTTP_USER_AGENT=navigator.userAgent,document.referrer.match(/\S/)&&(a.HTTP_REFERER=document.referrer),a);"string"==typeof e.cookies?c.HTTP_COOKIE=e.cookies:"object"===R(e.cookies)&&(c.HTTP_COOKIE=function(e){if("object"===R(e)){var n=[];for(var t in e)n.push(t+"="+e[t]);return n.join(";")}}(e.cookies));var s={notifier:k,error:{class:e.name,message:e.message,backtrace:e.stack,generator:o,fingerprint:e.fingerprint},request:{url:e.url,component:e.component,action:e.action,context:e.context,cgi_data:c,params:e.params},server:{project_root:e.projectRoot,environment_name:e.environment,revision:e.revision}};return w=s,b=e,E?(l("Deferring notice",e,s),window.setTimeout(function(){T(e)&&v(s)})):(l("Queuing notice",e,s),u.push(s)),e}var o=!0;if(window.atob||(o=!1),window.ErrorEvent)try{0===new window.ErrorEvent("").colno&&(o=!1)}catch(e){}function i(n,t){try{return"function"!=typeof n?n:(e=n,"function"!=typeof Object.isExtensible||Object.isExtensible(e)?(n.___hb||(n.___hb=function(){var e=d();if(!(o&&(e||t)||t&&!e))return n.apply(this,arguments);try{return n.apply(this,arguments)}catch(e){throw y(e),e}}),n.___hb.___hb=n.___hb,n.___hb):n)}catch(e){return n}var e}f.notify=function(e,n,t){if(e||(e={}),"[object Error]"===Object.prototype.toString.call(e)){var r=e;e=_(e,{name:r.name,message:r.message,stack:O(r)})}return"object"!==R(e)&&(e={message:String(e)}),n&&"object"!==R(n)&&(n={name:String(n)}),n&&(e=S(e,n)),"object"===R(t)&&(e=S(e,t)),y(e,function(e){var n;if(e&&(n=O(e)))return{stack:n,generator:void 0};try{throw new Error("")}catch(e){if(n=O(e))return{stack:n,generator:"throw"}}n=["<call-stack>"];for(var t=arguments.callee;t&&n.length<10;){/function(?:\s+([\w$]+))+\s*\(/.test(t.toString())?n.push(RegExp.$1||"<anonymous>"):n.push("<anonymous>");try{t=t.caller}catch(e){break}}return{stack:n.join("\n"),generator:"walk"}}(e))},f.wrap=function(e){return i(e,!0)},f.setContext=function(e){return"object"===R(e)&&(f.context=_(f.context,e)),f},f.resetContext=function(e){return"object"===R(e)?f.context=_({},e):f.context={},f},f.configure=function(e){for(var n in e)f[n]=e[n];return f},f.beforeNotify=function(e){return f.beforeNotifyHandlers.push(e),f};var a=[].indexOf||function(e){for(var n=0,t=this.length;n<t;n++)if(n in this&&this[n]===e)return n;return-1};function m(e,n,t){if(!c&&e&&n&&t){var r=e[n];e[n]=t(r)}}f.reset=function(){for(var e in f.context={},f.beforeNotifyHandlers=[],f)-1==a.call(n,e)&&(f[e]=void 0);return f.resetMaxErrors(),f},f.resetMaxErrors=function(){return f.errorsSent=0},f.getVersion=function(){return j};var g=function(r){return function(e,n){if("function"!=typeof e)return r(e,n);var t=Array.prototype.slice.call(arguments,2);return e=i(e),r(function(){e.apply(null,t)},n)}};for(var t in m(window,"setTimeout",g),m(window,"setInterval",g),"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(e){var n=window[e]&&window[e].prototype;n&&n.hasOwnProperty&&n.hasOwnProperty("addEventListener")&&(m(n,"addEventListener",function(o){return function(e,n,t,r){try{n&&null!=n.handleEvent&&(n.handleEvent=i(n.handleEvent))}catch(e){s(e)}return o.call(this,e,i(n),t,r)}}),m(n,"removeEventListener",function(o){return function(e,n,t,r){return o.call(this,e,n,t,r),o.call(this,e,i(n),t,r)}}))}),m(window,"onerror",function(i){return function(e,n,t,r,o){return function(e,n,t,r,o){if(l("window.onerror callback invoked",arguments),!b&&d())if(0===t&&/Script error\.?/.test(e))s("Ignoring cross-domain script error: enable CORS to track these types of errors",arguments);else{var i=[e,"\n at ? (",n||"unknown",":",t||0,":",r||0,")"].join("");if(o){var a={stack:O(o)};return a.stack||(a={stack:i}),y(o,a)}y({name:"window.onerror",message:e,stack:i})}}(e,n,t,r,o),!("function"!=typeof i||!p("_onerror_call_orig",!0))&&i.apply(this,arguments)}}),m(window,"onunhandledrejection",function(n){function t(e){if(l("window.onunhandledrejection callback invoked",arguments),!b&&!c&&p("onunhandledrejection",!0)){var n=e.reason;if(n instanceof Error){var t=n.fileName||"unknown",r=n.lineNumber||0,o="".concat(n.message,"\n at ? (").concat(t,":").concat(r,")"),i=O(n)||o;y({name:n.name,message:"UnhandledPromiseRejectionWarning: ".concat(n),stack:i})}else{var a="string"==typeof n?n:JSON.stringify(n);y({name:"window.onunhandledrejection",message:"UnhandledPromiseRejectionWarning: ".concat(a)})}}}return function(e){t(e),"function"==typeof n&&n.apply(this,arguments)}}),x=!0,f)n.push(t);if(l("Initializing honeybadger.js "+j),/complete|interactive|loaded/.test(document.readyState))E=!0,l("honeybadger.js 2.0.0 ready");else{l("Installing ready handler");var h=function(){var e;for(E=!0,l("honeybadger.js 2.0.0 ready");e=u.pop();)v(e)};document.addEventListener?document.addEventListener("DOMContentLoaded",h,!0):window.attachEvent("onload",h)}return f}}(),n=e();return n.factory=e,n}); | ||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Honeybadger=e()}(this,function(){"use strict";function T(t){return(T="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function N(t,i){var u=[];return function e(t,n){if(n||(n=0),i<=n)return"[DEPTH]";if(/function|symbol/.test(T(r=t))||"object"===T(r)&&void 0===r.hasOwnProperty)return Object.prototype.toString.call(t);var r;if(function(t){if(!t||"object"!==T(t))return!1;for(var e=0;e<u.length;e++)if(u[e]===t)return!0;return u.push(t),!1}(t))return"[RECURSION]";if(Array.isArray(t))return t.map(function(t){return e(t,n+1)});if("object"!==T(t))return t;var o={};for(var a in t){var c=t[a];Object.prototype.hasOwnProperty.call(t,a)&&null!=a&&null!=c&&(o[a]=e(c,n+1))}return o}(t)}function H(n){if(!n||!n.tagName)return"";var r=n.tagName.toLowerCase();if("html"===r)return"";n.id&&(r+="#".concat(n.id));var t=n.getAttribute("class");t&&t.split(/\s+/).forEach(function(t){r+=".".concat(t)}),["alt","name","title","type"].forEach(function(t){var e=n.getAttribute(t);e&&(r+="[".concat(t,'="').concat(e,'"]'))});var e=function(e){try{var t=e.parentNode.childNodes,n=[];return Array.prototype.forEach.call(t,function(t){t.tagName&&t.tagName===e.tagName&&n.push(t)}),n}catch(t){return[]}}(n);return 1<e.length&&(r+=":nth-child(".concat(Array.prototype.indexOf.call(e,n)+1,")")),r}function L(t){return-1!==t.toString().indexOf("native")}function r(t){var e=t.match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/)||{};return{protocol:e[2],host:e[4],pathname:e[5]}}function C(t){var e=r(t),n=r(document.URL);return!e.host||e.protocol?e.pathname:e.protocol===n.protocol&&e.host===n.host?e.pathname:"".concat(e.protocol,"://").concat(e.host).concat(e.pathname)}var t=function(){var b,w,k="2.1.0-beta.0",_={name:"honeybadger-js",url:"https://github.com/honeybadger-io/honeybadger-js",version:k,language:"javascript"},j=!1,x=!1;function E(t,e){var n={};for(var r in t)n[r]=t[r];for(var r in e)n[r]=e[r];return n}function S(t,e){var n=E(t,e);return t.context&&e.context&&(n.context=E(t.context,e.context)),n}function R(t){return!!b&&b.name===t.name&&b.message===t.message&&b.stack===t.stack}function O(t){return t.stacktrace||t.stack||void 0}return function(t){var u=x,e=[],s=[],f={context:{},beforeNotifyHandlers:[],breadcrumbs:[],errorsSent:0};if("object"===T(t))for(var n in t)f[n]=t[n];function i(){var t=window.console;if(t){var e=Array.prototype.slice.call(arguments);e.unshift("[Honeybadger]"),t.log.apply(t,e)}}function d(){if(l("debug"))return i.apply(this,arguments)}function l(t,e){var n=f[t];return void 0===n&&(n=f[t.toLowerCase()]),"false"===n&&(n=!1),void 0!==n?n:e}function p(){return!u&&l("onerror",!0)}function m(){return l("breadcrumbsEnabled",!1)}function r(t,e){try{var n=new XMLHttpRequest;n.open("POST","http"+(l("ssl",!0)?"s":"")+"://"+l("host","api.honeybadger.io")+"/v1/notices/js",l("async",!0)),n.setRequestHeader("X-API-Key",t),n.setRequestHeader("Content-Type","application/json"),n.setRequestHeader("Accept","text/json, application/json"),n.send(JSON.stringify(N(e,l("max_depth",8))))}catch(t){i("Unable to send error report: error while initializing request",t,e)}}function h(t){if(b=w=null,l("disabled",!1))return d("Dropping notice: honeybadger.js is disabled",t),!1;var e,n=l("apiKey",l("api_key"));return n?(e=l("maxErrors"))&&f.errorsSent>=e?(d("Dropping notice: max errors exceeded",t),!1):(f.errorsSent++,r(n,t),!0):(i("Unable to send error report: no API key has been configured"),!1)}function y(t,e){if(t||(t={}),"[object Error]"===Object.prototype.toString.call(t)){var n=t;t=E(t,{name:n.name,message:n.message,stack:O(n)})}if("object"!==T(t)){var r=String(t);t={message:r}}if(R(t))return!1;if(w&&j&&h(w),function(t){for(var e in t)if(Object.prototype.hasOwnProperty.call(t,e))return!1;return!0}(t))return!1;var o;e&&(t.stack=e.stack,o=e.generator),t=E(t,{name:t.name||"Error",context:E(f.context,t.context),url:t.url||document.URL,projectRoot:t.projectRoot||t.project_root||l("projectRoot",l("project_root",window.location.protocol+"//"+window.location.host)),environment:t.environment||l("environment"),component:t.component||l("component"),action:t.action||l("action"),revision:t.revision||l("revision")}),f.addBreadcrumb("Honeybadger Notice",{category:"notice",metadata:{message:t.message,name:t.name,stack:t.stack}}),t.breadcrumbs=f.breadcrumbs.slice();var a=t.stack;if(function(t,e){var n,r;for(n=0,r=t.length;n<r;n++)if(!1===(0,t[n])(e))return!0;return!1}(f.beforeNotifyHandlers,t))return!1;if(t.stack!=a&&(o=void 0),function(t,e){var n=t.message;for(var r in e)if(n.match(e[r]))return!0;return!1}(t,l("ignorePatterns")))return!1;var c,i=((c={}).HTTP_USER_AGENT=navigator.userAgent,document.referrer.match(/\S/)&&(c.HTTP_REFERER=document.referrer),c);"string"==typeof t.cookies?i.HTTP_COOKIE=t.cookies:"object"===T(t.cookies)&&(i.HTTP_COOKIE=function(t){if("object"===T(t)){var e=[];for(var n in t)e.push(n+"="+t[n]);return e.join(";")}}(t.cookies));var u={notifier:_,breadcrumbs:{enabled:m(),trail:t.breadcrumbs},error:{class:t.name,message:t.message,backtrace:t.stack,generator:o,fingerprint:t.fingerprint},request:{url:t.url,component:t.component,action:t.action,context:t.context,cgi_data:i,params:t.params},server:{project_root:t.projectRoot,environment_name:t.environment,revision:t.revision}};return w=u,b=t,j?(d("Deferring notice",t,u),window.setTimeout(function(){R(t)&&h(u)})):(d("Queuing notice",t,u),s.push(u)),t}var o=!0;if(window.atob||(o=!1),window.ErrorEvent)try{0===new window.ErrorEvent("").colno&&(o=!1)}catch(t){}function c(n,r){r||(r={});try{return"function"!=typeof n?n:(t=n,"function"!=typeof Object.isExtensible||Object.isExtensible(t)?(n.___hb||(n.___hb=function(){var t=p();if(!(o&&(t||r.force)||r.force&&!t))return n.apply(this,arguments);try{return n.apply(this,arguments)}catch(t){var e={stack:O(t)};throw f.addBreadcrumb(r.component?"".concat(r.component,": ").concat(t.name):t.name,{category:"error",metadata:{message:t.message,name:t.name,stack:e.stack}}),y(t,e),t}}),n.___hb.___hb=n.___hb,n.___hb):n)}catch(t){return n}var t}f.notify=function(t,e,n){if(t||(t={}),"[object Error]"===Object.prototype.toString.call(t)){var r=t;t=E(t,{name:r.name,message:r.message,stack:O(r)})}return"object"!==T(t)&&(t={message:String(t)}),e&&"object"!==T(e)&&(e={name:String(e)}),e&&(t=S(t,e)),"object"===T(n)&&(t=S(t,n)),y(t,function(t){var e;if(t&&(e=O(t)))return{stack:e,generator:void 0};try{throw new Error("")}catch(t){if(e=O(t))return{stack:e,generator:"throw"}}e=["<call-stack>"];for(var n=arguments.callee;n&&e.length<10;){/function(?:\s+([\w$]+))+\s*\(/.test(n.toString())?e.push(RegExp.$1||"<anonymous>"):e.push("<anonymous>");try{n=n.caller}catch(t){break}}return{stack:e.join("\n"),generator:"walk"}}(t))},f.wrap=function(t){return c(t,{force:!0})},f.setContext=function(t){return"object"===T(t)&&(f.context=E(f.context,t)),f},f.resetContext=function(t){return"object"===T(t)?f.context=E({},t):f.context={},f},f.configure=function(t){for(var e in t)f[e]=t[e];return f},f.beforeNotify=function(t){return f.beforeNotifyHandlers.push(t),f};var a=[].indexOf||function(t){for(var e=0,n=this.length;e<n;e++)if(e in this&&this[e]===t)return e;return-1};function g(t,e,n){if(!u&&t&&e&&n&&e in t){var r=t[e];t[e]=n(r)}}for(var n in f.reset=function(){for(var t in f.context={},f.beforeNotifyHandlers=[],f.breadcrumbs=[],f)-1==a.call(e,t)&&(f[t]=void 0);return f.resetMaxErrors(),f},f.resetMaxErrors=function(){return f.errorsSent=0},f.getVersion=function(){return k},f.addBreadcrumb=function(t,e){if(m()){var n=(e=e||{}).metadata||void 0,r=e.category||"custom",o=(new Date).toISOString();f.breadcrumbs.push({category:r,message:t,metadata:n||{},timestamp:o});var a=l("maxBreadcrumbs",40);return f.breadcrumbs.length>a&&(f.breadcrumbs=f.breadcrumbs.slice(f.breadcrumbs.length-a)),f}},window.addEventListener("click",function(t){var e,n,r,o,a,c;try{e=H(t.target),n=function t(e){var n=H(e);if(e.parentNode&&e.parentNode.tagName){var r=t(e.parentNode);if(0<r.length)return"".concat(r," > ").concat(n)}return n}(t.target),o=t.target,a=(o.textContent||o.innerText||o.value||"").trim(),c=300,a.length>c&&(a=a.substr(0,c)+"..."),r=a}catch(t){e="UI Click",r=n="[unknown]"}0!==e.length&&f.addBreadcrumb(e,{category:"ui.click",metadata:{selector:n,text:r,event:t}})},!0),g(XMLHttpRequest.prototype,"open",function(r){return function(){var t=arguments[1],e="string"==typeof arguments[0]?arguments[0].toUpperCase():arguments[0],n="".concat(e," ").concat(C(t));this.__hb_xhr={type:"xhr",method:e,url:t,message:n},"function"==typeof r&&r.apply(this,arguments)}}),g(XMLHttpRequest.prototype,"send",function(t){return function(){var e=this;function n(){var t;4===e.readyState&&(e.__hb_xhr&&(e.__hb_xhr.status_code=e.status,t=e.__hb_xhr.message,delete e.__hb_xhr.message),f.addBreadcrumb(t||"XMLHttpRequest",{category:"request",metadata:e.__hb_xhr}))}"onreadystatechange"in e&&"function"==typeof e.onreadystatechange?g(e,"onreadystatechange",function(t){return function(){n(),"function"==typeof t&&t.apply(this,arguments)}}):e.onreadystatechange=n,"function"==typeof t&&t.apply(e,arguments)}}),function(){if(!window.fetch)return!1;if(L(window.fetch))return!0;try{var t=document.createElement("iframe");t.style.display="none",document.head.appendChild(t);var e=t.contentWindow.fetch&&L(t.contentWindow.fetch);return document.head.removeChild(t),e}catch(t){console&&console.warn&&console.warn("failed to detect native fetch via iframe: "+t)}return!1}()&&g(window,"fetch",function(a){return function(){var t,e=arguments[0],n="GET";"string"==typeof e?t=e:"Request"in window&&e instanceof Request?(t=e.url,e.method&&(n=e.method)):t=String(e),arguments[1]&&arguments[1].method&&(n=arguments[1].method),"string"==typeof n&&(n=n.toUpperCase());var r="".concat(n," ").concat(C(t)),o={type:"fetch",method:n,url:t};return a.apply(this,arguments).then(function(t){return o.status_code=t.status,f.addBreadcrumb(r,{category:"request",metadata:o}),t}).catch(function(t){throw f.addBreadcrumb("fetch error",{category:"error",metadata:o}),t})}}),function(){var n=window.location.href;function r(t,e){n=e,f.addBreadcrumb("Page changed",{category:"navigation",metadata:{from:t,to:e}})}function t(e){return function(){var t=2<arguments.length?arguments[2]:void 0;return t&&r(n,String(t)),e.apply(this,arguments)}}g(window,"onpopstate",function(t){return function(){if(r(n,window.location.href),t)return t.apply(this,arguments)}}),g(window.history,"pushState",t),g(window.history,"replaceState",t)}(),["debug","info","warn","error","log"].forEach(function(a){g(window.console,a,function(o){return function(){var t,e=Array.prototype.slice.call(arguments),n=(t=e,Array.isArray(t)?t.map(function(t){try{return String(t)}catch(t){return"[unknown]"}}).join(" "):""),r={category:"log",metadata:{level:a,arguments:N(e,3)}};f.addBreadcrumb(n,r),"function"==typeof o&&Function.prototype.apply.call(o,window.console,arguments)}})}),function(){function t(o){return function(r){return function(t,e){if("function"!=typeof t)return r(t,e);var n=Array.prototype.slice.call(arguments,2);return t=c(t,o),r(function(){t.apply(null,n)},e)}}}g(window,"setTimeout",t({component:"setTimeout"})),g(window,"setInterval",t({component:"setInterval"}))}(),"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(t){var e=window[t]&&window[t].prototype;e&&e.hasOwnProperty&&e.hasOwnProperty("addEventListener")&&(g(e,"addEventListener",function(o){var a={component:"".concat(t,".prototype.addEventListener")};return function(t,e,n,r){try{e&&null!=e.handleEvent&&(e.handleEvent=c(e.handleEvent,a))}catch(t){i(t)}return o.call(this,t,c(e,a),n,r)}}),g(e,"removeEventListener",function(o){return function(t,e,n,r){return o.call(this,t,e,n,r),o.call(this,t,c(e),n,r)}}))}),g(window,"onerror",function(a){return function(t,e,n,r,o){return function(t,e,n,r,o){if(d("window.onerror callback invoked",arguments),!b&&p())if(0===n&&/Script error\.?/.test(t))i("Ignoring cross-domain script error: enable CORS to track these types of errors",arguments);else{var a,c=[t,"\n at ? (",e||"unknown",":",n||0,":",r||0,")"].join("");o?(a={stack:O(o)}).stack||(a={stack:c}):o={name:"window.onerror",message:t,stack:c},f.addBreadcrumb("window.onerror"!==o.name&&o.name?"window.onerror: ".concat(o.name):"window.onerror",{category:"error",metadata:{message:o.message,name:o.name,stack:a?a.stack:o.stack}}),y(o,a)}}(t,e,n,r,o),!("function"!=typeof a||!l("_onerror_call_orig",!0))&&a.apply(this,arguments)}}),g(window,"onunhandledrejection",function(e){function n(t){if(d("window.onunhandledrejection callback invoked",arguments),!b&&!u&&l("onunhandledrejection",!0)){var e=t.reason;if(e instanceof Error){var n=e.fileName||"unknown",r=e.lineNumber||0,o="".concat(e.message,"\n at ? (").concat(n,":").concat(r,")"),a=O(e)||o,c={name:e.name,message:"UnhandledPromiseRejectionWarning: ".concat(e),stack:a};return f.addBreadcrumb("window.onunhandledrejection: ".concat(c.name),{category:"error",metadata:c}),void y(c)}var i="string"==typeof e?e:JSON.stringify(e);y({name:"window.onunhandledrejection",message:"UnhandledPromiseRejectionWarning: ".concat(i)})}}return function(t){n(t),"function"==typeof e&&e.apply(this,arguments)}}),x=!0,f)e.push(n);if(d("Initializing honeybadger.js "+k),/complete|interactive|loaded/.test(document.readyState))j=!0,d("honeybadger.js "+k+" ready");else{d("Installing ready handler");var v=function(){var t;for(j=!0,d("honeybadger.js "+k+" ready");t=s.pop();)h(t)};document.addEventListener?document.addEventListener("DOMContentLoaded",v,!0):window.attachEvent("onload",v)}return f}}(),e=t();return e.factory=t,e}); | ||
//# sourceMappingURL=honeybadger.min.js.map |
@@ -14,2 +14,3 @@ declare module "honeybadger-js" { | ||
disabled?: boolean; | ||
maxErrors?: number; | ||
ignorePatterns?: RegExp[]; | ||
@@ -30,2 +31,3 @@ async?: boolean; | ||
context: any; | ||
cookies?: string; | ||
} | ||
@@ -32,0 +34,0 @@ |
{ | ||
"name": "honeybadger-js", | ||
"description": "A JavaScript library for integrating apps with the Honeybadger Rails Error Notifier.", | ||
"version": "2.0.0", | ||
"version": "2.1.0-beta.0", | ||
"license": "MIT", | ||
@@ -37,11 +37,19 @@ "homepage": "https://github.com/honeybadger-io/honeybadger-js", | ||
"@babel/preset-env": "^7.3.4", | ||
"jasmine": "^3.3.1", | ||
"jasmine-core": "^3.3.0", | ||
"karma": "^4.0.1", | ||
"babel-eslint": "^10.0.3", | ||
"eslint": "^6.1.0", | ||
"eslint-config-airbnb-base": "^14.0.0", | ||
"eslint-config-prettier": "^6.4.0", | ||
"eslint-plugin-import": "^2.18.2", | ||
"eslint-plugin-prettier": "^3.1.1", | ||
"jasmine": "^3.5.0", | ||
"jasmine-core": "^3.5.0", | ||
"karma": "^4.3.0", | ||
"karma-browserstack-launcher": "^1.5.1", | ||
"karma-chrome-launcher": "^3.0.0", | ||
"karma-jasmine": "^2.0.1", | ||
"karma-rollup-preprocessor": "^7.0.0", | ||
"karma-sauce-launcher": "^2.0.2", | ||
"karma-sinon": "^1.0.5", | ||
"puppeteer": "^1.13.0", | ||
"prettier": "^1.18.2", | ||
"promise-polyfill": "8.1.3", | ||
"puppeteer": "^1.20.0", | ||
"rollup": "^1.0.0", | ||
@@ -53,3 +61,4 @@ "rollup-plugin-babel": "^4.3.2", | ||
"rollup-plugin-uglify": "^6.0.2", | ||
"sinon": "^7.3.1" | ||
"sinon": "^7.3.1", | ||
"whatwg-fetch": "^3.0.0" | ||
}, | ||
@@ -56,0 +65,0 @@ "readmeFilename": "README.md", |
# Honeybadger Client-Side Javascript Library | ||
[![CircleCI](https://circleci.com/gh/honeybadger-io/honeybadger-js.svg?style=svg)](https://circleci.com/gh/honeybadger-io/honeybadger-js) | ||
[![Build Status](https://app.saucelabs.com/buildstatus/honeybadger_os)](https://app.saucelabs.com/builds/16ac6c22e2ea4140b3051bf21fb579da) | ||
@@ -28,4 +27,4 @@ A client-side JavaScript library for integrating apps with the :zap: [Honeybadger Error Notifier](http://honeybadger.io). For server-side javascript, check out our [NodeJS library](https://github.com/honeybadger-io/honeybadger-node). | ||
2. To run the test suite by itself, use `npm test`. | ||
3. To run the tests across all supported platforms, set up a [Sauce Labs](https://saucelabs.com/) | ||
account and use `SAUCE_USERNAME=your_username SAUCE_ACCESS_KEY=your-access-key npm run test:ci`. | ||
3. To run the tests across all supported platforms, set up a [BrowserStack](https://www.browserstack.com/) | ||
account and use `BROWSERSTACK_USERNAME=your_username BROWSERSTACK_ACCESS_KEY=your-access-key npm run test:ci`. | ||
@@ -63,1 +62,5 @@ ## Releasing | ||
The Honeybadger gem is MIT licensed. See the [MIT-LICENSE](https://raw.github.com/honeybadger-io/honeybadger-js/master/MIT-LICENSE) file in this repository for details. | ||
--- | ||
<p><a href="https://www.browserstack.com/"><img src="/browserstack-logo.png" width="150"></a><br> | ||
<small>We use <a href="https://www.browserstack.com/">BrowserStack</a> to run our automated integration tests on multiple platforms in CI.</small></p> |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
253126
3154
65
27
1