UNPKG

boomerangjs

Version:

boomerang always comes back, except when it hits something

1,487 lines (1,344 loc) 76.9 kB
/** * The `Errors` plugin automatically captures JavaScript and other errors from * your web application. * * This plugin has a corresponding {@tutorial header-snippets} that helps capture errors prior to Boomerang loading. * * For information on how to include this plugin, see the {@tutorial building} tutorial. * * ## Sources of Errors * * When the `Errors` plugin is enabled, the following sources of errors are captured: * * * JavaScript runtime errors captured via the * [`onerror`](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror) * global event handler * * [``XMLHttpRequest``](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) * responses that were not successful. Note {@link BOOMR.plugins.AutoXHR} is required * if using this. * * Any calls to [``window.console.error``](https://developer.mozilla.org/en-US/docs/Web/API/Console/error) * * JavaScript runtime errors that happen during a callback for * [``addEventListener``](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) * _(disabled by default, enabled via {@link BOOMR.plugins.Errors.init `monitorEvents`})_ * * JavaScript runtime errors that happen during a callback for * [``setTimeout``](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout) * and [``setInterval``](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval) * _(disabled by default, enabled via {@link BOOMR.plugins.Errors.init `monitorTimeout`})_ * * Manually sent errors via {@link BOOMR.plugins.Errors.send} * * Functions that threw an exception that were wrapped via {@link BOOMR.plugins.Errors.wrap} * * Functions that threw an exception that were run via {@link BOOMR.plugins.Errors.test} * * JavaScript runtime errors captured via the * [`unhandledrejection`](https://developer.mozilla.org/en-US/docs/Web/Events/unhandledrejection) * global event handler _(disabled by default, enabled via {@link BOOMR.plugins.Errors.init `monitorRejections`})_ * * JavaScript runtime warnings captured via the * [`Reporting API`](https://www.w3.org/TR/reporting/#reporting-observer) * _(disabled by default, enabled via {@link BOOMR.plugins.Errors.init `monitorReporting`})_ * * All of the options above can be * {@link BOOMR.plugins.Errors.init manually turned off}. * * ## Supported Browsers * * The `Errors` plugin can be enabled for all browsers, though some older browsers * may not be able to capture the full breadth of sources of errors. Due to the lack * of error detail on some older browsers, some errors may be reported more than once. * * Notable browsers: * * * Internet Explorer <= 8: Does not support capturing `XMLHttpRequest` errors. * * ## Manually Sending Errors * * Besides automatically capturing errors from `onerror`, `XMLHttpRequest`, * `console.error` or event handlers such as `setTimeout`, you can also manually * send errors. * * There are three ways of doing this as follows: * * * {@link BOOMR.plugins.Errors.send}: Immediately sends an error. * * {@link BOOMR.plugins.Errors.wrap}: Wraps a function with error tracking * * {@link BOOMR.plugins.Errors.test}: Runs the function and captures any errors. * * ## Error callback * * You can specify an {@link BOOMR.plugins.Errors.init `onError`} function that * the Errors plugin will call any time an error is captured on the page. * * If your `onError` function returns `true`, the error will be captured. * * If your `onError` function does not return `true`, the error will be ignored. * * Example: * * ``` * BOOMR.init({ * Errors: { * onError: function(err) { * if (err.message && err.message.indexOf("internally handled")) { * return false; * } * return true; * } * } * }); * ``` * * ## When to Send Errors * * By default, errors captured during the page load will be sent along with the * page load beacon. * * Errors that happen after the page load will not be captured or sent. * * To enable capturing of errors after page load, you need to set * {@link BOOMR.plugins.Errors.init `sendAfterOnload`} to `true`. If set, * errors that happen after the page load will be sent at most once every * {@link BOOMR.plugins.Errors.init `sendInterval`} (which defaults to 1 second) * on a new beacon. * * Example: * * ``` * BOOMR.init({ * Errors: { * sendAfterOnload: true, * sendInterval: 5000 * } * }); * ``` * * ## How Many Errors to Capture * * The `Errors` plugin will only capture up to * {@link BOOMR.plugins.Errors.init `maxErrors`} (defaults to 10) distinct * errors on the page. * * Please note that duplicate errors (those with the same function name, stack, * and so on) are tracked as single distinct error, with a `count` of how many * times it was seen. * * You can increase (or decrease) `maxErrors`. For example: * ``` * BOOMR.init({ * Errors: { * maxErrors: 20 * } * }); * ``` * * ## Dealing with Script Error * * When looking at JavaScript errors, you will likely come across the generic error * message: `Script error.` * * `Script Error.` is the message that browsers send to the * [`window.onerror`](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror) * global exception handler when the error was triggered by a script loaded from a different (cross) * [origin](https://developer.mozilla.org/en-US/docs/Glossary/Origin). `window.onerror` * is used by Boomerang so that it gets notified of all unhandled exceptions. * * The `Script Error.` string is given _instead_ of the real error message * and does not contain any useful information about what caused the error. In addition, * there is no stack associated with the message, so it's impossible to know where * or why the error occurred. * * Browsers mask the real error message for cross-origin scripts due to security and * privacy concerns - they don't want to leak sensitive information in the message * or stack. The only thing that `window.onerror` knows for cross-origin scripts * is that an error occurred, not where or why. * * ### Example * * For an example of where you'd see `Script Error.`, consider the following code * that lives on `website.com`: * * ```html * <html> * <head> * <title>website.com</title> * </head> * <body> * <script> * window.onerror = function(message, url, line, column, error) { * console.log("window.onerror: " + message); * console.log((error && error.stack) ? error.stack : "(no stack)"); * }; * </script> * <script src="my-script.js"></script> * <script src="https://anothersite.com/my-script.js"></script> * </body> * </html> * ``` * * Assume `my-script.js` is the same file being served from both `website.com` and * `anothersite.com`: * * ```javascript * function runCode() { * a = b + 1; * } * * runCode(); * ``` * * When `my-script.js` is loaded from `website.com`, it will be executed twice: * * 1. First on the same-origin, where we'll see the full error message followed by * the stack: * * ``` * window.onerror: Uncaught ReferenceError: b is not defined * * ReferenceError: b is not defined * at runCode (my-script.js:2) * at my-script.js:5 * ``` * * 2. Then, it will be loaded from `https://anothersite.com/my-script.js`, which * will be considered cross-origin and only `Script Error.` will be logged: * * ``` * window.onerror: Script error. * * (no stack) * ``` * * As you can see, browser shares the full details of the exception when it's served * from the same origin as the website, but if it's served from any other origin, * it will be considered cross-origin and no details will be shared. * * Note that while the browser only shares `Script Error.` to `window.onerror` * for cross-origin scripts, if _you_ have browser developer tools open, * the browser will show _you_ the full error message in the Console. This is * because there aren't any security or privacy concerns for a developer looking at * their own machine's information. * * ### When You'll See Script Error * * Unfortunately `Script Error.` will be shown in many legitimate use-cases, * such as: * * 1. When serving your website's JavaScript from a CDN (since it will be coming * from a different origin) * * 2. When loading a library such as jQuery or Angular from their CDN, i.e. * [Google's Hosted Libraries](https://developers.google.com/speed/libraries/) * or [cdnjs](https://cdnjs.com/) * * 3. When a third-party script loads from another domain * * The good news is that in many of these cases, there are changes you can make to * ensure the full error message and stack are shared with `window.onerror`. * * ### Fixing Script Error * * To ensure a cross-origin script shares full error details with `window.onerror`, * you'll need to do **two** things: * * 1. Add `crossorigin="anonymous"` to the `<script>` tag * * The [`crossorigin="anonymous"` attrib](https://www.w3.org/TR/html5/infrastructure.html#cors-settings-attribute) * tells the browser that the script should be fetched without sending * any cookies or HTTP authentication * * 2. Add the `Access-Control-Allow-Origin` (ACAO) header to the JavaScript file's response. * * The `Access-Control-Allow-Origin` header is part of the * [Cross Origin Resource Sharing](https://www.w3.org/TR/cors/) (CORS) standard. * * The ACAO header **must** be set in the JavaScript's HTTP response headers. * * An example header that sets ACAO for all calling origins would be: * * `Access-Control-Allow-Origin: *` * * If both conditions are true, cross-origin JavaScript files will report errors * to `window.onerror` with the correct error message and full stack. * * The biggest challenge to getting this working is that (1) is within the _site's_ * control while (2) can only be configured by the _owner_ of the JavaScript. If you're * loading JavaScript from a third-party, you will need to encourage them to add the * ACAO header if it's not already set. The good news is that many CDNs and * third-parties set the ACAO header already. * * ### Workarounds for Third Parties that aren't sending ACAO * * One way to help monitor for errors coming from third-party scripts that aren't * setting ACAO (and aren't within your control) is by manually wrapping calls * to any of the third-party script's functions in a `try {} catch {}`. * * ```javascript * try { * // calls a cross-origin script that doesn't have ACAO * runThirdPartyCode(); * } catch (e) { * // report on error with e.message and e.stack * } * ``` * * If `runThirdPartyCode()` causes any errors, the `catch {}` handler will get the full * details of the exception. * * Unfortunately this won't work for functions that are executed in the third-party * script as a result of browser events or callbacks (since you're not wrapping them). * * When using Boomerang to monitor JavaScript errors, Boomerang automatically wraps some * of the built-in browser APIs such as `setTimeout`, `setInterval` * (via the {@link BOOMR.plugins.Errors.init `monitorTimeout`} option) * and `addEventListener` (via the {@link BOOMR.plugins.Errors.init `monitorEvents`} option) * with a minimal-overhead wrapper. It does this to help ensure as many cross-origin * exceptions as possible have full stack details. You may also do this manually via * [`` BOOMR.plugin.Errors.wrap(function)``](#BOOMR_plugin_Errors_wrap). * * Note that enabling {@link BOOMR.plugins.Errors.init `monitorTimeout`} or * {@link BOOMR.plugins.Errors.init `monitorEvents`} can have side-effects and has caused * compatibility issues with JavaScript code on some sites. Enabling those options is only * recommended after verifying there are no problems. * * ## Why is Boomerang in my Error Stack? * * When looking at error reports, you may find errors that have a function in * `boomerang.js` (or `/boomerang/`) on the stack. Why is that? Is Boomerang * causing errors on your site? * * One of the ways that Boomerang is able to monitor and measure your site's performance * is by _wrapping_ itself around some of the core browser APIs. Boomerang only does * this in a few places, if absolutely necessary -- namely, when the browser doesn't * provide a native "monitoring" interface for something that needs to be tracked. * * One example is for `XMLHttpRequests`, as there are no browser APIs to monitor when * XHRs load. To monitor XHRs, Boomerang swaps in its own `window.XMLHttpRequest` * object, wrapping around the native methods. When an XHR is created (via `.open()`), * the lightweight Boomerang wrapper is executed first so it can log a start timestamp. * When the XHR finishes (via a `readyState` change), Boomerang can log the end * timestamp and report on the XHR's performance. * * Examples of Boomerang wrapping native methods include: * * * `XMLHttpRequest` if the XHR instrumentation is turned on * * `console.error` if error tracking is turned on * * `setTimeout` and `setInterval`(if error tracking is turned on * (with {@link BOOMR.plugins.Errors.init `monitorTimeout`}) * * `addEventListener` and `removeEventListener` (if error tracking is turned on * (with {@link BOOMR.plugins.Errors.init `monitorEvents`}) * * All of these wrapped functions come into play when you see an error stack with * a `boomerang.js` function in it. * * Often, the `boomerang.js` function will be at the _bottom_ of the stack (the first * function called). This does not mean Boomerang caused the error, merely that * the monitoring code was running before the error occurred. The actual * error happens towards the _top_ of the stack -- the function that ran and threw * the exception. * * Let's look at some examples: * * ``` * Cannot read property 'foo' of undefined at thirdPartyTwo (https://thirdparty.com/core.js:1:100) * at thirdPartyOne (https://thirdparty.com/core.js:1:101) * at runThirdParty (https://thirdparty.com/core.js:1:102) * at xhrCallback (http://website.com/site.js:2:200) * at XMLHttpRequest.send (https://mysite.com/boomerang.js:3:300) * ``` * * In the above example, Boomerang is monitoring `XMLHttpRequests`. An XHR was * loaded on the site, and during the XHR callback, an exception was thrown. Even * though `/boomerang/` is listed here, the error was caused by code in the XHR * callback (`xhrCallback` eventually calling `thirdPartyTwo`). * * Here's a second example: * * ``` * Reference error: a is not defined at setTimeout (http://website.com/site.js:1:200) * at BOOMR_plugins_errors_wrap (http://mysite.com/boomerang.js:3:300) * at onclick (http://website.com/site.js:1:100) * ``` * * In the above example, JavaScript Error Reporting is enabled and an exception was * thrown in a `setTimeout()` on the website. You can see the `BOOMR_plugins_errors_wrap` * function is near the top of the stack, but this is merely the error tracking code. * All it did was wrap `setTimeout` to help ensure that cross-origin exceptions are * caught. It was not the actual cause of the site's error. * * Here's a third example: * * ``` * Error: missing argument 1 at BOOMR.window.console.error (https://mysite.com/boomerang.js:3:300) * at u/< (https://website.com/site.js:1:100) * at tp/this.$get</< (https://website.com/site.js:1:200) * at $digest (https://website.com/site.js:1:300) * at $apply (https://website.com/site.js:1:400) * at ut (https://website.com/site.js:1:500) * at it (https://website.com/site.js:1:600) * at vp/</k.onload (https://website.com/site.js:1:700) * ``` * * In the above example, JavaScript Error Reporting is enabled and has wrapped * `console.error`. The minified function `u/<` must be logging a `console.error`, * which executes the Boomerang wrapper code, reporting the error. * * In summary, if you see Boomerang functions in error stacks similar to any of the * ones listed below, it's probable that you're just seeing a side-effect of the * monitoring code: * * * `BOOMR_addError` * * `BOOMR_plugins_errors_onerror` * * `BOOMR_plugins_errors_onxhrerror` * * `BOOMR_plugins_errors_console_error` * * `BOOMR_plugins_errors_wrap` * * `BOOMR.window.console.error` * * `BOOMR_plugins_errors_onrejection` * * ## Side Effects * * Enabling wrapping through {@link BOOMR.plugins.Errors.init `monitorEvents`} and * {@link BOOMR.plugins.Errors.init `monitorTimeout`} may trigger some side effects: * * * Boomerang's monitoring code will be run first for every callback, which will add minimal (though non-zero) * overhead. * * In browser console logs, errors that are triggered by other libraries that have been wrapped * will now look like they come from Boomerang instead, as Boomerang is now on the bottom of the call stack. * * Browser developer tools such as Chrome's Performance and Profiler tabs may be confused about * JavaScript CPU attribution. In other words, they may think Boomerang is the cause of more work than it is. * * Chrome Lighthouse may be confused about JavaScript CPU attribution, due to the same reasons as above. * * WebPagetest may be confused about JavaScript CPU attribution, due to the same reasons as above. * * There are some cases where JavaScript applications may have compatibility issues with the wrapping. Some * notable cases include: * * Use of the global `window.event` object, [see issue](https://github.com/whatwg/dom/issues/735). * * Other libraries that wrap `setTimeout`, `addEventListener`, etc such as `history.js`. * * Pages that use the `<base href="...">` tag. * * For more details, you can read this * [article](https://nicj.net/side-effects-of-boomerangs-javascript-error-tracking/). * * ## Beacon Parameters * * * `err`: The compressed error data structure * * `http.initiator = error` (if not part of a Page Load beacon) * * The compressed error data structure is a [JSURL](https://github.com/Sage/jsurl) * encoded JSON object. * * Each element in the array is a compressed representation of a JavaScript error: * * * `n`: Count (if the error was seen more than once) * * `f[]`: An array of frames * * `f[].l`: Line number * * `f[].c`: Colum number * * `f[].f`: Function name * * `f[].w`: File name (if origin differs from root page) * * `f[].wo`: File name without origin (if same as root page) * * `s`: Source: * * `1`: Error was triggered by the application * * `2`: Error was triggered by Boomerang * * `v`: Via * * `1`: Application ({@link BOOMR.plugins.Errors.send}) * * `2`: Global exception handler (`window.onerror`) * * `3`: Network (XHR) error * * `4`: Console (`console.error`) * * `5`: Event handler (`addEventListener`) * * `6`: `setTimeout` or `setInterval` * * `t`: Type (e.g. `SyntaxError` or `ReferenceError`) * * `c`: Code (for network errors) * * `m`: Error messag * * `x`: Extra data * * `d`: Timestamp (base 36) * * @class BOOMR.plugins.Errors */ /*eslint-disable*/ // // Via https://github.com/stacktracejs/error-stack-parser // Modifications: // * Removed UMD // * Return anonymous objects, not StackFrames // (function (root, factory) { 'use strict'; root.ErrorStackParser = factory(); }(this, function ErrorStackParser() { 'use strict'; var FIREFOX_SAFARI_STACK_REGEXP = /(^|@)\S+\:\d+/; var CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+\:\d+|\(native\))/m; var SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code\])?$/; function _map(array, fn, thisArg) { if (typeof Array.prototype.map === 'function') { return array.map(fn, thisArg); } else { var output = new Array(array.length); for (var i = 0; i < array.length; i++) { output[i] = fn.call(thisArg, array[i]); } return output; } } function _filter(array, fn, thisArg) { if (typeof Array.prototype.filter === 'function') { return array.filter(fn, thisArg); } else { var output = []; for (var i = 0; i < array.length; i++) { if (fn.call(thisArg, array[i])) { output.push(array[i]); } } return output; } } return { /** * Given an Error object, extract the most information from it. * @param error {Error} * @return Array[] * @ignore */ parse: function ErrorStackParser$$parse(error) { if (typeof error.stacktrace !== 'undefined' || typeof error['opera#sourceloc'] !== 'undefined') { return this.parseOpera(error); } else if (error.stack && error.stack.match(CHROME_IE_STACK_REGEXP)) { return this.parseV8OrIE(error); } else if (error.stack) { return this.parseFFOrSafari(error); } else { throw new Error('Cannot parse given Error object'); } }, /** * Separate line and column numbers from a URL-like string. * @param urlLike String * @return Array[String] * @ignore */ extractLocation: function ErrorStackParser$$extractLocation(urlLike) { // Fail-fast but return locations like "(native)" if (urlLike.indexOf(':') === -1) { return [urlLike]; } var locationParts = urlLike.replace(/[\(\)\s]/g, '').split(':'); var lastNumber = locationParts.pop(); var possibleNumber = locationParts[locationParts.length - 1]; if (!isNaN(parseFloat(possibleNumber)) && isFinite(possibleNumber)) { var lineNumber = locationParts.pop(); return [locationParts.join(':'), lineNumber, lastNumber]; } else { return [locationParts.join(':'), lastNumber, undefined]; } }, parseV8OrIE: function ErrorStackParser$$parseV8OrIE(error) { var filtered = _filter(error.stack.split('\n'), function (line) { return !!line.match(CHROME_IE_STACK_REGEXP); }, this); return _map(filtered, function (line) { if (line.indexOf('(eval ') > -1) { // Throw away eval information until we implement stacktrace.js/stackframe#8 line = line.replace(/eval code/g, 'eval').replace(/(\(eval at [^\()]*)|(\)\,.*$)/g, ''); } var tokens = line.replace(/^\s+/, '').replace(/\(eval code/g, '(').split(/\s+/).slice(1); var locationParts = this.extractLocation(tokens.pop()); var functionName = tokens.join(' ') || undefined; var fileName = locationParts[0] === 'eval' ? undefined : locationParts[0]; return { functionName: functionName, fileName: fileName, lineNumber: locationParts[1], columnNumber: locationParts[2], source: line }; }, this); }, parseFFOrSafari: function ErrorStackParser$$parseFFOrSafari(error) { var filtered = _filter(error.stack.split('\n'), function (line) { return !line.match(SAFARI_NATIVE_CODE_REGEXP); }, this); return _map(filtered, function (line) { // Throw away eval information until we implement stacktrace.js/stackframe#8 if (line.indexOf(' > eval') > -1) { line = line.replace(/ line (\d+)(?: > eval line \d+)* > eval\:\d+\:\d+/g, ':$1'); } if (line.indexOf('@') === -1 && line.indexOf(':') === -1) { // Safari eval frames only have function names and nothing else return { functionName: line }; } else { var tokens = line.split('@'); var locationParts = this.extractLocation(tokens.pop()); var functionName = tokens.join('@') || undefined; return { functionName: functionName, fileName: locationParts[0], lineNumber: locationParts[1], columnNumber: locationParts[2], source: line }; } }, this); }, parseOpera: function ErrorStackParser$$parseOpera(e) { if (!e.stacktrace || (e.message.indexOf('\n') > -1 && e.message.split('\n').length > e.stacktrace.split('\n').length)) { return this.parseOpera9(e); } else if (!e.stack) { return this.parseOpera10(e); } else { return this.parseOpera11(e); } }, parseOpera9: function ErrorStackParser$$parseOpera9(e) { var lineRE = /Line (\d+).*script (?:in )?(\S+)/i; var lines = e.message.split('\n'); var result = []; for (var i = 2, len = lines.length; i < len; i += 2) { var match = lineRE.exec(lines[i]); if (match) { result.push({ fileName: match[2], lineNumber: match[1], source: lines[i] }); } } return result; }, parseOpera10: function ErrorStackParser$$parseOpera10(e) { var lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i; var lines = e.stacktrace.split('\n'); var result = []; for (var i = 0, len = lines.length; i < len; i += 2) { var match = lineRE.exec(lines[i]); if (match) { result.push({ functionName: match[3] || undefined, fileName: match[2], lineNumber: match[1], source: lines[i] }); } } return result; }, // Opera 10.65+ Error.stack very similar to FF/Safari parseOpera11: function ErrorStackParser$$parseOpera11(error) { var filtered = _filter(error.stack.split('\n'), function (line) { return !!line.match(FIREFOX_SAFARI_STACK_REGEXP) && !line.match(/^Error created at/); }, this); return _map(filtered, function (line) { var tokens = line.split('@'); var locationParts = this.extractLocation(tokens.pop()); var functionCall = (tokens.shift() || ''); var functionName = functionCall .replace(/<anonymous function(: (\w+))?>/, '$2') .replace(/\([^\)]*\)/g, '') || undefined; var argsRaw; if (functionCall.match(/\(([^\)]*)\)/)) { argsRaw = functionCall.replace(/^[^\(]+\(([^\)]*)\)$/, '$1'); } var args = (argsRaw === undefined || argsRaw === '[arguments not available]') ? undefined : argsRaw.split(','); return { functionName: functionName, args: args, fileName: locationParts[0], lineNumber: locationParts[1], columnNumber: locationParts[2], source: line }; }, this); } }; })); /* eslint-enable */ /** * Boomerang Error plugin */ (function() { var impl; BOOMR = window.BOOMR || {}; BOOMR.plugins = BOOMR.plugins || {}; if (BOOMR.plugins.Errors) { return; } // // Constants // /** * Functions to strip from any stack (internal functions) */ var STACK_FUNCTIONS_REMOVE = [ "BOOMR_addError", "createStackForSend", "BOOMR.window.console.error", "BOOMR.plugins.Errors.init", "BOOMR.window.onerror", // below matches multiple functions: "BOOMR_plugins_errors_" ]; // functions to strip if they match a STACK_FILENAME_MATCH var STACK_FUNCTIONS_REMOVE_IF_FILENAME_MATCH = [ "Object.send", "b.send", "wrap", "Anonymous function" ]; // files that will match for STACK_FUNCTIONS_REMOVE_IF_FILENAME_MATCH var STACK_FILENAME_MATCH = [ "/boomerang" ]; /** * Maximum size, in characters, of stack to capture */ var MAX_STACK_SIZE = 5000; /** * BoomerangError object * * @param {object} config Configuration */ function BoomerangError(config) { config = config || {}; // how many times we've seen this error if (typeof config.count === "number" || typeof config.count === "string") { this.count = parseInt(config.count, 10); } else { this.count = 1; } if (typeof config.timestamp === "number") { this.timestamp = config.timestamp; } else { this.timestamp = BOOMR.now(); } // merge in properties from config if (typeof config.code === "number" || typeof config.code === "string") { this.code = parseInt(config.code, 10); } if (typeof config.message === "string") { this.message = config.message; } if (typeof config.functionName === "string") { this.functionName = config.functionName; } if (typeof config.fileName === "string") { this.fileName = config.fileName; } if (typeof config.lineNumber === "number" || typeof config.lineNumber === "string") { this.lineNumber = parseInt(config.lineNumber, 10); } if (typeof config.columnNumber === "number" || typeof config.columnNumber === "string") { this.columnNumber = parseInt(config.columnNumber, 10); } if (typeof config.stack === "string") { this.stack = config.stack; } if (typeof config.type === "string") { this.type = config.type; } if (typeof config.extra !== "undefined") { this.extra = config.extra; } this.source = (typeof config.source === "number" || typeof config.source === "string") ? parseInt(config.source, 10) : BOOMR.plugins.Errors.SOURCE_APP; if (typeof config.via === "number" || typeof config.via === "string") { this.via = parseInt(config.via, 10); } if (BOOMR.utils.isArray(config.frames)) { this.frames = config.frames; } else { this.frames = []; } if (BOOMR.utils.isArray(config.events)) { this.events = config.events; } else { this.events = []; } } /** * Determines if one BoomerangError object is equal to another * * @param {object} other Object to compare to * * @returns {boolean} True if the two objects are logically equal errors */ BoomerangError.prototype.equals = function(other) { if (typeof other !== "object") { return false; } else if (this.code !== other.code) { return false; } else if (this.message !== other.message) { return false; } else if (this.functionName !== other.functionName) { return false; } else if (this.fileName !== other.fileName) { return false; } else if (this.lineNumber !== other.lineNumber) { return false; } else if (this.columnNumber !== other.columnNumber) { return false; } else if (this.stack !== other.stack) { return false; } else if (this.type !== other.type) { return false; } else if (this.source !== other.source) { return false; } else { // same! return true; } }; /** * Creates a BoomerangError from an Error * * @param {Error} error Error object * @param {number} via How the Error was found (VIA_* enum) * @param {number} source Source of the error (SOURCE_* enum) * * @returns {BoomerangError} Error */ BoomerangError.fromError = function(error, via, source) { var frame, frames, lastFrame, forceUpdate = false, i, j, k, now = BOOMR.now(), skipThis, thisFrame, thisFn; if (!error) { return null; } // parse the stack if (error.stack) { if (error.stack.length > MAX_STACK_SIZE) { error.stack = error.stack.substr(0, MAX_STACK_SIZE); } frames = ErrorStackParser.parse(error); if (frames && frames.length) { if (error.generatedStack) { // if we generated the stack (we were only given a message), // we should remove our stack-generation function from it // fix-up stack generation on Chrome if (frames.length >= 4 && frames[1].functionName && frames[1].functionName.indexOf("createStackForSend") !== -1) { // remove the top 3 frames frames = frames.slice(3); forceUpdate = true; } // fix-up stack generation on Firefox if (frames.length >= 3 && frames[0].functionName && frames[0].functionName.indexOf("createStackForSend") !== -1) { // check to see if the filename of frames two and 3 are the same (boomerang), // if so, remove both if (frames[1].fileName === frames[2].fileName) { // remove the top 3 frames frames = frames.slice(3); } else { // remove the top 2 frames frames = frames.slice(2); } forceUpdate = true; } // strip other stack generators if (frames.length >= 1 && frames[0].functionName && frames[0].functionName.indexOf("BOOMR_plugins_errors") !== -1) { frames = frames.slice(1); forceUpdate = true; } } // remove our error wrappers from the stack for (i = 0; i < frames.length; i++) { thisFrame = frames[i]; thisFn = thisFrame.functionName; skipThis = false; // strip boomerang function names if (thisFn) { for (j = 0; j < STACK_FUNCTIONS_REMOVE.length; j++) { if (thisFn.indexOf(STACK_FUNCTIONS_REMOVE[j]) !== -1) { frames.splice(i, 1); forceUpdate = true; // outloop continues with the next element i--; skipThis = true; break; } } // strip additional functions if they also match a file if (!skipThis && thisFrame.fileName) { for (j = 0; j < STACK_FILENAME_MATCH.length; j++) { if (thisFrame.fileName.indexOf(STACK_FILENAME_MATCH[j]) !== -1) { // this file name matches, see if any of the matching functions also do for (k = 0; k < STACK_FUNCTIONS_REMOVE_IF_FILENAME_MATCH.length; k++) { if (thisFn.indexOf(STACK_FUNCTIONS_REMOVE_IF_FILENAME_MATCH[k]) !== -1) { frames.splice(i, 1); forceUpdate = true; // outloop continues with the next element i--; skipThis = true; break; } } } } } } } if (frames.length) { // get the top frame frame = frames[0]; // fill in our error with the top frame, if not already specified if (forceUpdate || typeof error.lineNumber === "undefined") { error.lineNumber = frame.lineNumber; } if (forceUpdate || typeof error.columnNumber === "undefined") { error.columnNumber = frame.columnNumber; } if (forceUpdate || typeof error.functionName === "undefined") { error.functionName = frame.functionName; } if (forceUpdate || typeof error.fileName === "undefined") { error.fileName = frame.fileName; } } // trim stack down if (error.stack) { // remove double-spaces error.stack = error.stack.replace(/\s\s+/g, " "); } } } else if (error.functionName || error.fileName || error.lineNumber || error.columnNumber) { // reconstruct a single frame if given fileName, etc frames = [{ lineNumber: error.lineNumber, columnNumber: error.columnNumber, fileName: error.fileName, functionName: error.functionName }]; } // fixup some old browser types if (typeof error.message === "string" && typeof error.message.indexOf === "function" && error.message.indexOf("ReferenceError:") !== -1 && error.name === "Error") { error.name = "ReferenceError"; } // create our final object var err = new BoomerangError({ code: error.code ? error.code : undefined, message: error.message ? error.message : undefined, functionName: error.functionName ? error.functionName : undefined, fileName: error.fileName ? error.fileName : undefined, lineNumber: error.lineNumber ? error.lineNumber : undefined, columnNumber: error.columnNumber ? error.columnNumber : undefined, stack: error.stack ? error.stack : undefined, type: error.name ? error.name : undefined, source: source, via: via, frames: frames, extra: error.extra ? error.extra : undefined, timestamp: error.timestamp ? error.timestamp : now }); return err; }; // // Internal config // impl = { // // Configuration // // overridable onError: undefined, monitorGlobal: true, monitorNetwork: true, monitorConsole: true, // can cause compat issues, off by default monitorEvents: false, // can cause compat issues, off by default monitorTimeout: false, // new feature, off by default monitorRejections: false, // new feature, off by default monitorReporting: false, sendAfterOnload: false, maxErrors: 10, // How often to send an error beacon after onload sendInterval: 1000, // How often to send a beacon during onload if autorun=false sendIntervalDuringLoad: 2500, sendIntervalId: -1, maxEvents: 10, // state isDuringLoad: true, initialized: false, autorun: true, /** * All errors */ errors: [], /** * Errors queued up for the next batch */ q: [], /** * Circular event buffer */ events: [], // Reporting API observer reportingObserver: undefined, // // Public Functions // /** * Sends an error * * @param {Error|String} error Error object or message * * @memberof BOOMR.plugins.Errors */ send: function(error, via, source) { var now = BOOMR.now(); if (!error) { return; } // check if this error was already sent. // This could happen if an event handler caught it and then the global // error handler caught it again. if (error.reported === true) { return; } error.reported = true; // defaults, if not specified via = via || BOOMR.plugins.Errors.VIA_APP; source = source || BOOMR.plugins.Errors.SOURCE_APP; // if we weren't given a stack, try to create one if (!error.stack && !error.noStack) { // run this in a function so we can detect it easier by the name, // and remove it from any stack frames we send function createStackForSend() { try { throw Error(error); } catch (ex) { error = ex; // note we generated this stack for later error.generatedStack = true; // set the time when it was created error.timestamp = error.timestamp || now; impl.addError(error, via, source); } } createStackForSend(); } else { // add the timestamp error.timestamp = error.timestamp || now; // send (or queue) the error impl.addError(error, via, source); } }, // // Private Functions // /** * Sends (or queues) errors * * @param {Error} error Error * @param {number} via VIA_* constant * @param {number} source SOURCE_* constant */ addError: function(error, via, source) { var onErrorResult, err, dup = false, now = BOOMR.now(); // only track post-load errors if configured if (!impl.isDuringLoad && !impl.sendAfterOnload) { return; } // allow the user to filter out the error if (impl.onError) { try { onErrorResult = impl.onError(error); } catch (exc) { onErrorResult = false; } if (!onErrorResult) { return; } } // obey the errors limit if (impl.errors.length >= impl.maxErrors) { return; } // convert into our object err = BoomerangError.fromError(error, via, source); // add to our list of errors seen for all time dup = impl.mergeDuplicateErrors(impl.errors, err, false); // fire an error event with the duped or new error BOOMR.fireEvent("error", dup || err); // add to our current queue impl.mergeDuplicateErrors(impl.q, err, true); // // There are a few reasons we'll send an error beacon on its own: // 1. If this is after onload, and sendAfterOnload is set. // 2. If this is during onload, but autorun is false. In that case, // we want to send out errors (after a small delay) in case the // page never loads (e.g. due to the error). // if ((BOOMR.hasSentPageLoadBeacon() || !impl.autorun) && impl.sendIntervalId === -1) { if (dup) { // If this is not during a load, and it's a duplicate of // a previous error, don't send a beacon just for itself return; } if (impl.isDuringLoad && impl.sendIntervalDuringLoad <= 0){ // if this is during a load and sending during load has been // disabled via config, do not try to send return; } // errors outside of a load will be sent at the next interval impl.sendIntervalId = setTimeout(function() { impl.sendIntervalId = -1; // Don't send a beacon if we've already flushed the queue. This // might happen for pre-onload becaons if the onload beacon was // sent after queueing if (impl.q.length === 0) { return; } // Queue a beacon whenever there isn't another one ongoing BOOMR.sendBeaconWhenReady( { // change this to an 'error' beacon "rt.start": "manual", "http.initiator": "error", // set it as an API beacon, which means it won't have any timing data "api": 1, // when "rt.tstart": now, "rt.end": now }, function() { // add our errors to the beacon when ready impl.addErrorsToBeacon(); }, this); }, impl.isDuringLoad ? impl.sendIntervalDuringLoad : impl.sendInterval); } }, /** * Finds a duplicate BoomerangErrors in the specified array * * @param {Array[]} errors Array of BoomerangErrors * @param {BoomerangError} err BoomerangError to check * * @returns {BoomerangError} BoomerangErrors that was duped against, if any */ findDuplicateError: function(errors, err) { if (!BOOMR.utils.isArray(errors) || typeof err === "undefined") { return undefined; } for (var i = 0; i < errors.length; i++) { if (errors[i].equals(err)) { return errors[i]; } } return undefined; }, /** * Merges duplicate BoomerangErrors * * @param {Array[]} errors Array of BoomerangErrors * @param {BoomerangError} err BoomerangError to check * @param {boolean} bumpCount Increment the count of any found duplicates * * @returns {BoomerangError} BoomerangErrors that was duped against, if any */ mergeDuplicateErrors: function(errors, err, bumpCount) { if (!BOOMR.utils.isArray(errors) || typeof err === "undefined") { return undefined; } var dup = impl.findDuplicateError(errors, err); if (dup) { if (bumpCount) { dup.count += err.count; } return dup; } else { errors.push(err); return undefined; } }, /** * Fired on 'page_ready' */ pageReady: function() { impl.isDuringLoad = false; }, /** * Retrieves the current errors * * @returns {BoomerangError[]} */ getErrors: function() { if (impl.errors.length === 0) { return false; } return impl.errors; }, /** * Gets errors suitable for transmission in a URL * * @param {BoomerangError[]} errors BoomerangErrors array * * @returns {string} String for URL */ getErrorsForUrl: function(errors) { errors = impl.compressErrors(errors); return BOOMR.utils.serializeForUrl(errors); }, /** * Adds any queue'd errors to the beacon */ addErrorsToBeacon: function() { if (impl.q.length) { var err = this.getErrorsForUrl(impl.q); if (err) { BOOMR.addVar("err", err, true); } impl.q = []; } }, /** * Fired 'before_beacon' */ beforeBeacon: function(vars) { // Add errors to all beacon types except early beacons if (!vars || typeof vars.early === "undefined") { impl.addErrorsToBeacon(); } }, /** * Wraps calls to functionName in an exception handler that will * automatically report exceptions. * * @param {string} functionName Function name * @param {object} that Target object * @param {boolean} useCallingObject Whether or not to use the calling object for 'this' * @param {number} callbackIndex Which argument is the callback * @param {number} via Via */ wrapFn: function(functionName, that, useCallingObject, callbackIndex, via) { var origFn = that[functionName]; if (typeof origFn !== "function") { return; } var rEL; if (functionName === "addEventListener") { // grab the native rEL = that.removeEventListener; } BOOMR.utils.overwriteNative(that, functionName, function BOOMR_plugins_errors_wrapped_function() { try { var args = Array.prototype.slice.call(arguments); var callbackFn = args[callbackIndex]; // Determine the calling object: if 'this' is the Boomerang frame, we should swap it // to the correct top level window context. If Boomerang isn't running in a frame, // BOOMR.window will still point to the top-level window. var targetObj = useCallingObject ? (this === window ? BOOMR.window : this) : that; var wrappedFn = impl.wrap(callbackFn, targetObj, via); args[callbackIndex] = wrappedFn; if (functionName === "addEventListener") { // For removeEventListener we need to keep track of this // unique tuple of target object, event name (arg0), original function // and capture (arg2) // Since we wrap the origFn with a new anonymous function we can't rely on // the browser's addEventListener to dedup multiple additions of the same // callback. if (!impl.trackFn(targetObj, args[0], callbackFn, args[2], wrappedFn)) { // if the callback is already tracked, we won't call addEventListener return; } if (rEL) { // Remove the listener before adding it back in. // This takes care of the (pathological) case where code is relying on the native // de-dupping that the browser provides and BOOMR instruments `addEventListener` between // their redundant calls to `addEventListener`. // We detach with the native because there's no point in calling our wrapped version. rEL.apply(targetObj, arguments); } } return origFn.apply(targetObj, args); } catch (e) { // error during original callback setup impl.send(e, via); // re-throw throw e; } }); }, /** * Tracks the specified function for removeEventListener. * * @param {object} target Target element (window, element, etc) * @param {string} type Event type (name) * @param {function} listener Original listener * @param {boolean|object} useCapture|options Use capture flag or options object * @param {function} wrapped Wrapped function * * @returns {boolean} `true` if function is not already tracked, false otherwise */ trackFn: function(target, type, listener, useCapture, wrapped) { if (!target) { return false; } if (impl.trackedFnIdx(target, type, listener, useCapture) !== -1) { // already tracked return false; } if (!target._bmrEvents) { target._bmrEvents = []; } // 3rd argument can be useCapture flag or options object that may contain a `capture` key. // Default is false in both cases useCapture = (useCapture && useCapture.capture || useCapture) === true; target._bmrEvents.push([type, listener, useCapture, wrapped]); return true; }, /** * Gets the index of the tracked function. * * @param {object} target Target element (window, element, etc) * @param {string} type Event type (name) * @param {function} listener Original listener * @param {boolean|object} useCapture|options Use capture flag or options object * * @returns {number} Index of already tracked function, or -1 if it doesn't exist */ trackedFnIdx: function(target, type, listener, useCapture) { var i, f; if (!target) { return; } if (!target._bmrEvents) { target._bmrEvents = []; } // 3rd argument can be useCapture flag or options object that may contain a `capture` key. // Default is false in both cases useCapture = (useCapture && useCapture.capture ||