UNPKG

rollbar

Version:

Effortlessly track and debug errors in your JavaScript applications with Rollbar. This package includes advanced error tracking features and an intuitive interface to help you identify and fix issues more quickly.

993 lines (927 loc) 29 kB
var _ = require('../utility'); var headers = require('../utility/headers'); var replace = require('../utility/replace'); var scrub = require('../scrub'); var urlparser = require('./url'); var domUtil = require('./domUtility'); var defaults = { network: true, networkResponseHeaders: false, networkResponseBody: false, networkRequestHeaders: false, networkRequestBody: false, networkErrorOnHttp5xx: false, networkErrorOnHttp4xx: false, networkErrorOnHttp0: false, log: true, dom: true, navigation: true, connectivity: true, contentSecurityPolicy: true, errorOnContentSecurityPolicy: false, }; function restore(replacements, type) { var b; while (replacements[type].length) { b = replacements[type].shift(); b[0][b[1]] = b[2]; } } function nameFromDescription(description) { if (!description || !description.attributes) { return null; } var attrs = description.attributes; for (var a = 0; a < attrs.length; ++a) { if (attrs[a].key === 'name') { return attrs[a].value; } } return null; } function defaultValueScrubber(scrubFields) { var patterns = []; for (var i = 0; i < scrubFields.length; ++i) { patterns.push(new RegExp(scrubFields[i], 'i')); } return function (description) { var name = nameFromDescription(description); if (!name) { return false; } for (var i = 0; i < patterns.length; ++i) { if (patterns[i].test(name)) { return true; } } return false; }; } function Instrumenter(options, telemeter, rollbar, _window, _document) { this.options = options; var autoInstrument = options.autoInstrument; if (options.enabled === false || autoInstrument === false) { this.autoInstrument = {}; } else { if (!_.isType(autoInstrument, 'object')) { autoInstrument = defaults; } this.autoInstrument = _.merge(defaults, autoInstrument); } this.scrubTelemetryInputs = !!options.scrubTelemetryInputs; this.telemetryScrubber = options.telemetryScrubber; this.defaultValueScrubber = defaultValueScrubber(options.scrubFields); this.telemeter = telemeter; this.rollbar = rollbar; this.diagnostic = rollbar.client.notifier.diagnostic; this._window = _window || {}; this._document = _document || {}; this.replacements = { network: [], log: [], navigation: [], connectivity: [], }; this.eventRemovers = { dom: [], connectivity: [], contentsecuritypolicy: [], }; this._location = this._window.location; this._lastHref = this._location && this._location.href; } Instrumenter.prototype.configure = function (options) { this.options = _.merge(this.options, options); var autoInstrument = options.autoInstrument; var oldSettings = _.merge(this.autoInstrument); if (options.enabled === false || autoInstrument === false) { this.autoInstrument = {}; } else { if (!_.isType(autoInstrument, 'object')) { autoInstrument = defaults; } this.autoInstrument = _.merge(defaults, autoInstrument); } this.instrument(oldSettings); if (options.scrubTelemetryInputs !== undefined) { this.scrubTelemetryInputs = !!options.scrubTelemetryInputs; } if (options.telemetryScrubber !== undefined) { this.telemetryScrubber = options.telemetryScrubber; } }; // eslint-disable-next-line complexity Instrumenter.prototype.instrument = function (oldSettings) { if (this.autoInstrument.network && !(oldSettings && oldSettings.network)) { this.instrumentNetwork(); } else if ( !this.autoInstrument.network && oldSettings && oldSettings.network ) { this.deinstrumentNetwork(); } if (this.autoInstrument.log && !(oldSettings && oldSettings.log)) { this.instrumentConsole(); } else if (!this.autoInstrument.log && oldSettings && oldSettings.log) { this.deinstrumentConsole(); } if (this.autoInstrument.dom && !(oldSettings && oldSettings.dom)) { this.instrumentDom(); } else if (!this.autoInstrument.dom && oldSettings && oldSettings.dom) { this.deinstrumentDom(); } if ( this.autoInstrument.navigation && !(oldSettings && oldSettings.navigation) ) { this.instrumentNavigation(); } else if ( !this.autoInstrument.navigation && oldSettings && oldSettings.navigation ) { this.deinstrumentNavigation(); } if ( this.autoInstrument.connectivity && !(oldSettings && oldSettings.connectivity) ) { this.instrumentConnectivity(); } else if ( !this.autoInstrument.connectivity && oldSettings && oldSettings.connectivity ) { this.deinstrumentConnectivity(); } if ( this.autoInstrument.contentSecurityPolicy && !(oldSettings && oldSettings.contentSecurityPolicy) ) { this.instrumentContentSecurityPolicy(); } else if ( !this.autoInstrument.contentSecurityPolicy && oldSettings && oldSettings.contentSecurityPolicy ) { this.deinstrumentContentSecurityPolicy(); } }; Instrumenter.prototype.deinstrumentNetwork = function () { restore(this.replacements, 'network'); }; Instrumenter.prototype.instrumentNetwork = function () { var self = this; function wrapProp(prop, xhr) { if (prop in xhr && _.isFunction(xhr[prop])) { replace(xhr, prop, function (orig) { return self.rollbar.wrap(orig); }); } } if ('XMLHttpRequest' in this._window) { var xhrp = this._window.XMLHttpRequest.prototype; replace( xhrp, 'open', function (orig) { return function (method, url) { var isUrlObject = _isUrlObject(url); if (_.isType(url, 'string') || isUrlObject) { url = isUrlObject ? url.toString() : url; if (this.__rollbar_xhr) { this.__rollbar_xhr.method = method; this.__rollbar_xhr.url = url; this.__rollbar_xhr.status_code = null; this.__rollbar_xhr.start_time_ms = _.now(); this.__rollbar_xhr.end_time_ms = null; } else { this.__rollbar_xhr = { method: method, url: url, status_code: null, start_time_ms: _.now(), end_time_ms: null, }; } } return orig.apply(this, arguments); }; }, this.replacements, 'network', ); replace( xhrp, 'setRequestHeader', function (orig) { return function (header, value) { // If xhr.open is async, __rollbar_xhr may not be initialized yet. if (!this.__rollbar_xhr) { this.__rollbar_xhr = {}; } if (_.isType(header, 'string') && _.isType(value, 'string')) { if (self.autoInstrument.networkRequestHeaders) { if (!this.__rollbar_xhr.request_headers) { this.__rollbar_xhr.request_headers = {}; } this.__rollbar_xhr.request_headers[header] = value; } // We want the content type even if request header telemetry is off. if (header.toLowerCase() === 'content-type') { this.__rollbar_xhr.request_content_type = value; } } return orig.apply(this, arguments); }; }, this.replacements, 'network', ); replace( xhrp, 'send', function (orig) { /* eslint-disable no-unused-vars */ return function (data) { /* eslint-enable no-unused-vars */ var xhr = this; function onreadystatechangeHandler() { if (xhr.__rollbar_xhr) { if (xhr.__rollbar_xhr.status_code === null) { xhr.__rollbar_xhr.status_code = 0; if (self.autoInstrument.networkRequestBody) { xhr.__rollbar_xhr.request = data; } xhr.__rollbar_event = self.captureNetwork( xhr.__rollbar_xhr, 'xhr', undefined, ); } if (xhr.readyState < 2) { xhr.__rollbar_xhr.start_time_ms = _.now(); } if (xhr.readyState > 3) { xhr.__rollbar_xhr.end_time_ms = _.now(); var headers = null; xhr.__rollbar_xhr.response_content_type = xhr.getResponseHeader('Content-Type'); if (self.autoInstrument.networkResponseHeaders) { var headersConfig = self.autoInstrument.networkResponseHeaders; headers = {}; try { var header, i; if (headersConfig === true) { var allHeaders = xhr.getAllResponseHeaders(); if (allHeaders) { var arr = allHeaders.trim().split(/[\r\n]+/); var parts, value; for (i = 0; i < arr.length; i++) { parts = arr[i].split(': '); header = parts.shift(); value = parts.join(': '); headers[header] = value; } } } else { for (i = 0; i < headersConfig.length; i++) { header = headersConfig[i]; headers[header] = xhr.getResponseHeader(header); } } } catch (e) { /* we ignore the errors here that could come from different * browser issues with the xhr methods */ } } var body = null; if (self.autoInstrument.networkResponseBody) { try { body = xhr.responseText; } catch (e) { /* ignore errors from reading responseText */ } } var response = null; if (body || headers) { response = {}; if (body) { if ( self.isJsonContentType( xhr.__rollbar_xhr.response_content_type, ) ) { response.body = self.scrubJson(body); } else { response.body = body; } } if (headers) { response.headers = headers; } } if (response) { xhr.__rollbar_xhr.response = response; } try { var code = xhr.status; code = code === 1223 ? 204 : code; xhr.__rollbar_xhr.status_code = code; xhr.__rollbar_event.level = self.telemeter.levelFromStatus(code); self.errorOnHttpStatus(xhr.__rollbar_xhr); } catch (e) { /* ignore possible exception from xhr.status */ } } } } wrapProp('onload', xhr); wrapProp('onerror', xhr); wrapProp('onprogress', xhr); if ( 'onreadystatechange' in xhr && _.isFunction(xhr.onreadystatechange) ) { replace(xhr, 'onreadystatechange', function (orig) { return self.rollbar.wrap( orig, undefined, onreadystatechangeHandler, ); }); } else { xhr.onreadystatechange = onreadystatechangeHandler; } if (xhr.__rollbar_xhr && self.trackHttpErrors()) { xhr.__rollbar_xhr.stack = new Error().stack; } return orig.apply(this, arguments); }; }, this.replacements, 'network', ); } if ('fetch' in this._window) { replace( this._window, 'fetch', function (orig) { /* eslint-disable no-unused-vars */ return function (fn, t) { /* eslint-enable no-unused-vars */ var args = new Array(arguments.length); for (var i = 0, len = args.length; i < len; i++) { args[i] = arguments[i]; } var input = args[0]; var method = 'GET'; var url; var isUrlObject = _isUrlObject(input); if (_.isType(input, 'string') || isUrlObject) { url = isUrlObject ? input.toString() : input; } else if (input) { url = input.url; if (input.method) { method = input.method; } } if (args[1] && args[1].method) { method = args[1].method; } var metadata = { method: method, url: url, status_code: null, start_time_ms: _.now(), end_time_ms: null, }; if (args[1] && args[1].headers) { // Argument may be a Headers object, or plain object. Ensure here that // we are working with a Headers object with case-insensitive keys. var reqHeaders = headers(args[1].headers); metadata.request_content_type = reqHeaders.get('Content-Type'); if (self.autoInstrument.networkRequestHeaders) { metadata.request_headers = self.fetchHeaders( reqHeaders, self.autoInstrument.networkRequestHeaders, ); } } if (self.autoInstrument.networkRequestBody) { if (args[1] && args[1].body) { metadata.request = args[1].body; } else if ( args[0] && !_.isType(args[0], 'string') && args[0].body ) { metadata.request = args[0].body; } } self.captureNetwork(metadata, 'fetch', undefined); if (self.trackHttpErrors()) { metadata.stack = new Error().stack; } // Start our handler before returning the promise. This allows resp.clone() // to execute before other handlers touch the response. return orig.apply(this, args).then(function (resp) { metadata.end_time_ms = _.now(); metadata.status_code = resp.status; metadata.response_content_type = resp.headers.get('Content-Type'); var headers = null; if (self.autoInstrument.networkResponseHeaders) { headers = self.fetchHeaders( resp.headers, self.autoInstrument.networkResponseHeaders, ); } var body = null; if (self.autoInstrument.networkResponseBody) { if (typeof resp.text === 'function') { // Response.text() is not implemented on some platforms // The response must be cloned to prevent reading (and locking) the original stream. // This must be done before other handlers touch the response. body = resp.clone().text(); //returns a Promise } } if (headers || body) { metadata.response = {}; if (body) { // Test to ensure body is a Promise, which it should always be. if (typeof body.then === 'function') { body.then(function (text) { if ( text && self.isJsonContentType(metadata.response_content_type) ) { metadata.response.body = self.scrubJson(text); } else { metadata.response.body = text; } }); } else { metadata.response.body = body; } } if (headers) { metadata.response.headers = headers; } } self.errorOnHttpStatus(metadata); return resp; }); }; }, this.replacements, 'network', ); } }; Instrumenter.prototype.captureNetwork = function ( metadata, subtype, rollbarUUID, ) { if ( metadata.request && this.isJsonContentType(metadata.request_content_type) ) { metadata.request = this.scrubJson(metadata.request); } return this.telemeter.captureNetwork(metadata, subtype, rollbarUUID); }; Instrumenter.prototype.isJsonContentType = function (contentType) { return contentType && _.isType(contentType, 'string') && contentType.toLowerCase().includes('json') ? true : false; }; Instrumenter.prototype.scrubJson = function (json) { return JSON.stringify(scrub(JSON.parse(json), this.options.scrubFields)); }; Instrumenter.prototype.fetchHeaders = function (inHeaders, headersConfig) { var outHeaders = {}; try { var i; if (headersConfig === true) { if (typeof inHeaders.entries === 'function') { // Headers.entries() is not implemented in IE var allHeaders = inHeaders.entries(); var currentHeader = allHeaders.next(); while (!currentHeader.done) { outHeaders[currentHeader.value[0]] = currentHeader.value[1]; currentHeader = allHeaders.next(); } } } else { for (i = 0; i < headersConfig.length; i++) { var header = headersConfig[i]; outHeaders[header] = inHeaders.get(header); } } } catch (e) { /* ignore probable IE errors */ } return outHeaders; }; Instrumenter.prototype.trackHttpErrors = function () { return ( this.autoInstrument.networkErrorOnHttp5xx || this.autoInstrument.networkErrorOnHttp4xx || this.autoInstrument.networkErrorOnHttp0 ); }; Instrumenter.prototype.errorOnHttpStatus = function (metadata) { var status = metadata.status_code; if ( (status >= 500 && this.autoInstrument.networkErrorOnHttp5xx) || (status >= 400 && this.autoInstrument.networkErrorOnHttp4xx) || (status === 0 && this.autoInstrument.networkErrorOnHttp0) ) { var error = new Error('HTTP request failed with Status ' + status); error.stack = metadata.stack; this.rollbar.error(error, { skipFrames: 1 }); } }; Instrumenter.prototype.deinstrumentConsole = function () { if (!('console' in this._window && this._window.console.log)) { return; } var b; while (this.replacements['log'].length) { b = this.replacements['log'].shift(); this._window.console[b[0]] = b[1]; } }; Instrumenter.prototype.instrumentConsole = function () { if (!('console' in this._window && this._window.console.log)) { return; } var self = this; var c = this._window.console; function wrapConsole(method) { 'use strict'; // See https://github.com/rollbar/rollbar.js/pull/778 var orig = c[method]; var origConsole = c; var level = method === 'warn' ? 'warning' : method; c[method] = function () { var args = Array.prototype.slice.call(arguments); var message = _.formatArgsAsString(args); self.telemeter.captureLog(message, level, null, _.now()); if (orig) { Function.prototype.apply.call(orig, origConsole, args); } }; self.replacements['log'].push([method, orig]); } var methods = ['debug', 'info', 'warn', 'error', 'log']; try { for (var i = 0, len = methods.length; i < len; i++) { wrapConsole(methods[i]); } } catch (e) { this.diagnostic.instrumentConsole = { error: e.message }; } }; Instrumenter.prototype.deinstrumentDom = function () { if (!('addEventListener' in this._window || 'attachEvent' in this._window)) { return; } this.removeListeners('dom'); }; Instrumenter.prototype.instrumentDom = function () { if (!('addEventListener' in this._window || 'attachEvent' in this._window)) { return; } var clickHandler = this.handleClick.bind(this); var blurHandler = this.handleBlur.bind(this); this.addListener('dom', this._window, 'click', 'onclick', clickHandler, true); this.addListener( 'dom', this._window, 'blur', 'onfocusout', blurHandler, true, ); }; Instrumenter.prototype.handleClick = function (evt) { try { var e = domUtil.getElementFromEvent(evt, this._document); var hasTag = e && e.tagName; var anchorOrButton = domUtil.isDescribedElement(e, 'a') || domUtil.isDescribedElement(e, 'button'); if ( hasTag && (anchorOrButton || domUtil.isDescribedElement(e, 'input', ['button', 'submit'])) ) { this.captureDomEvent('click', e); } else if (domUtil.isDescribedElement(e, 'input', ['checkbox', 'radio'])) { this.captureDomEvent('input', e, e.value, e.checked); } } catch (exc) { // TODO: Not sure what to do here } }; Instrumenter.prototype.handleBlur = function (evt) { try { var e = domUtil.getElementFromEvent(evt, this._document); if (e && e.tagName) { if (domUtil.isDescribedElement(e, 'textarea')) { this.captureDomEvent('input', e, e.value); } else if ( domUtil.isDescribedElement(e, 'select') && e.options && e.options.length ) { this.handleSelectInputChanged(e); } else if ( domUtil.isDescribedElement(e, 'input') && !domUtil.isDescribedElement(e, 'input', [ 'button', 'submit', 'hidden', 'checkbox', 'radio', ]) ) { this.captureDomEvent('input', e, e.value); } } } catch (exc) { // TODO: Not sure what to do here } }; Instrumenter.prototype.handleSelectInputChanged = function (elem) { if (elem.multiple) { for (var i = 0; i < elem.options.length; i++) { if (elem.options[i].selected) { this.captureDomEvent('input', elem, elem.options[i].value); } } } else if (elem.selectedIndex >= 0 && elem.options[elem.selectedIndex]) { this.captureDomEvent('input', elem, elem.options[elem.selectedIndex].value); } }; Instrumenter.prototype.captureDomEvent = function ( subtype, element, value, isChecked, ) { if (value !== undefined) { if ( this.scrubTelemetryInputs || domUtil.getElementType(element) === 'password' ) { value = '[scrubbed]'; } else { var description = domUtil.describeElement(element); if (this.telemetryScrubber) { if (this.telemetryScrubber(description)) { value = '[scrubbed]'; } } else if (this.defaultValueScrubber(description)) { value = '[scrubbed]'; } } } var elementString = domUtil.elementArrayToString( domUtil.treeToArray(element), ); this.telemeter.captureDom(subtype, elementString, value, isChecked); }; Instrumenter.prototype.deinstrumentNavigation = function () { var chrome = this._window.chrome; var chromePackagedApp = chrome && chrome.app && chrome.app.runtime; // See https://github.com/angular/angular.js/pull/13945/files var hasPushState = !chromePackagedApp && this._window.history && this._window.history.pushState; if (!hasPushState) { return; } restore(this.replacements, 'navigation'); }; Instrumenter.prototype.instrumentNavigation = function () { var chrome = this._window.chrome; var chromePackagedApp = chrome && chrome.app && chrome.app.runtime; // See https://github.com/angular/angular.js/pull/13945/files var hasPushState = !chromePackagedApp && this._window.history && this._window.history.pushState; if (!hasPushState) { return; } var self = this; replace( this._window, 'onpopstate', function (orig) { return function () { var current = self._location.href; self.handleUrlChange(self._lastHref, current); if (orig) { orig.apply(this, arguments); } }; }, this.replacements, 'navigation', ); replace( this._window.history, 'pushState', function (orig) { return function () { var url = arguments.length > 2 ? arguments[2] : undefined; if (url) { self.handleUrlChange(self._lastHref, url + ''); } return orig.apply(this, arguments); }; }, this.replacements, 'navigation', ); }; Instrumenter.prototype.handleUrlChange = function (from, to) { var parsedHref = urlparser.parse(this._location.href); var parsedTo = urlparser.parse(to); var parsedFrom = urlparser.parse(from); this._lastHref = to; if ( parsedHref.protocol === parsedTo.protocol && parsedHref.host === parsedTo.host ) { to = parsedTo.path + (parsedTo.hash || ''); } if ( parsedHref.protocol === parsedFrom.protocol && parsedHref.host === parsedFrom.host ) { from = parsedFrom.path + (parsedFrom.hash || ''); } this.telemeter.captureNavigation(from, to, null, _.now()); }; Instrumenter.prototype.deinstrumentConnectivity = function () { if (!('addEventListener' in this._window || 'body' in this._document)) { return; } if (this._window.addEventListener) { this.removeListeners('connectivity'); } else { restore(this.replacements, 'connectivity'); } }; Instrumenter.prototype.instrumentConnectivity = function () { if (!('addEventListener' in this._window || 'body' in this._document)) { return; } if (this._window.addEventListener) { this.addListener( 'connectivity', this._window, 'online', undefined, function () { this.telemeter.captureConnectivityChange('online'); }.bind(this), true, ); this.addListener( 'connectivity', this._window, 'offline', undefined, function () { this.telemeter.captureConnectivityChange('offline'); }.bind(this), true, ); } else { var self = this; replace( this._document.body, 'ononline', function (orig) { return function () { self.telemeter.captureConnectivityChange('online'); if (orig) { orig.apply(this, arguments); } }; }, this.replacements, 'connectivity', ); replace( this._document.body, 'onoffline', function (orig) { return function () { self.telemeter.captureConnectivityChange('offline'); if (orig) { orig.apply(this, arguments); } }; }, this.replacements, 'connectivity', ); } }; Instrumenter.prototype.handleCspEvent = function (cspEvent) { var message = 'Security Policy Violation: ' + 'blockedURI: ' + cspEvent.blockedURI + ', ' + 'violatedDirective: ' + cspEvent.violatedDirective + ', ' + 'effectiveDirective: ' + cspEvent.effectiveDirective + ', '; if (cspEvent.sourceFile) { message += 'location: ' + cspEvent.sourceFile + ', ' + 'line: ' + cspEvent.lineNumber + ', ' + 'col: ' + cspEvent.columnNumber + ', '; } message += 'originalPolicy: ' + cspEvent.originalPolicy; this.telemeter.captureLog(message, 'error', null, _.now()); this.handleCspError(message); }; Instrumenter.prototype.handleCspError = function (message) { if (this.autoInstrument.errorOnContentSecurityPolicy) { this.rollbar.error(message); } }; Instrumenter.prototype.deinstrumentContentSecurityPolicy = function () { if (!('addEventListener' in this._document)) { return; } this.removeListeners('contentsecuritypolicy'); }; Instrumenter.prototype.instrumentContentSecurityPolicy = function () { if (!('addEventListener' in this._document)) { return; } var cspHandler = this.handleCspEvent.bind(this); this.addListener( 'contentsecuritypolicy', this._document, 'securitypolicyviolation', null, cspHandler, false, ); }; Instrumenter.prototype.addListener = function ( section, obj, type, altType, handler, capture, ) { if (obj.addEventListener) { obj.addEventListener(type, handler, capture); this.eventRemovers[section].push(function () { obj.removeEventListener(type, handler, capture); }); } else if (altType) { obj.attachEvent(altType, handler); this.eventRemovers[section].push(function () { obj.detachEvent(altType, handler); }); } }; Instrumenter.prototype.removeListeners = function (section) { var r; while (this.eventRemovers[section].length) { r = this.eventRemovers[section].shift(); r(); } }; function _isUrlObject(input) { return typeof URL !== 'undefined' && input instanceof URL; } module.exports = Instrumenter;