boomerangjs
Version:
boomerang always comes back, except when it hits something
1,487 lines (1,344 loc) • 76.9 kB
JavaScript
/**
* 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 ||