diff --git a/js/tracekit/tracekit.js b/js/tracekit/tracekit.js index 78bb33be54..7d91404f2b 100644 --- a/js/tracekit/tracekit.js +++ b/js/tracekit/tracekit.js @@ -1,10 +1,12 @@ -/* - TraceKit - Cross brower stack traces - github.com/occ/TraceKit - MIT license -*/ - -;(function(window, undefined) { - +/** + * https://github.com/csnover/TraceKit + * @license MIT + * @namespace TraceKit + */ +(function(window, undefined) { +if (!window) { + return; +} var TraceKit = {}; var _oldTraceKit = window.TraceKit; @@ -13,25 +15,34 @@ var _oldTraceKit = window.TraceKit; var _slice = [].slice; var UNKNOWN_FUNCTION = '?'; - /** - * _has, a better form of hasOwnProperty - * Example: _has(MainHostObject, property) === true/false + * A better form of hasOwnProperty
+ * Example: `_has(MainHostObject, property) === true/false` * - * @param {Object} host object to check property + * @param {Object} object to check property * @param {string} key to check + * @return {Boolean} true if the object has the key and it is not inherited */ function _has(object, key) { return Object.prototype.hasOwnProperty.call(object, key); } +/** + * Returns true if the parameter is undefined
+ * Example: `_isUndefined(val) === true/false` + * + * @param {*} what Value to check + * @return {Boolean} true if undefined and false otherwise + */ function _isUndefined(what) { return typeof what === 'undefined'; } /** - * TraceKit.noConflict: Export TraceKit out to another variable - * Example: var TK = TraceKit.noConflict() + * Export TraceKit out to another variable
+ * Example: `var TK = TraceKit.noConflict()` + * @return {Object} The TraceKit object + * @memberof TraceKit */ TraceKit.noConflict = function noConflict() { window.TraceKit = _oldTraceKit; @@ -39,11 +50,12 @@ TraceKit.noConflict = function noConflict() { }; /** - * TraceKit.wrap: Wrap any function in a TraceKit reporter - * Example: func = TraceKit.wrap(func); + * Wrap any function in a TraceKit reporter
+ * Example: `func = TraceKit.wrap(func);` * * @param {Function} func Function to be wrapped * @return {Function} The wrapped func + * @memberof TraceKit */ TraceKit.wrap = function traceKitWrapper(func) { function wrapped() { @@ -58,23 +70,25 @@ TraceKit.wrap = function traceKitWrapper(func) { }; /** - * TraceKit.report: cross-browser processing of unhandled exceptions + * Cross-browser processing of unhandled exceptions * * Syntax: + * ```js * TraceKit.report.subscribe(function(stackInfo) { ... }) * TraceKit.report.unsubscribe(function(stackInfo) { ... }) * TraceKit.report(exception) * try { ...code... } catch(ex) { TraceKit.report(ex); } + * ``` * * Supports: * - Firefox: full stack trace with line numbers, plus column number - * on top frame; column number is not guaranteed - * - Opera: full stack trace with line and column numbers - * - Chrome: full stack trace with line and column numbers - * - Safari: line and column number for the top frame only; some frames - * may be missing, and column number is not guaranteed - * - IE: line and column number for the top frame only; some frames - * may be missing, and column number is not guaranteed + * on top frame; column number is not guaranteed + * - Opera: full stack trace with line and column numbers + * - Chrome: full stack trace with line and column numbers + * - Safari: line and column number for the top frame only; some frames + * may be missing, and column number is not guaranteed + * - IE: line and column number for the top frame only; some frames + * may be missing, and column number is not guaranteed * * In theory, TraceKit should work on all of the following versions: * - IE5.5+ (only 8.0 tested) @@ -93,17 +107,22 @@ TraceKit.wrap = function traceKitWrapper(func) { * If the exception does not reach the top of the browser, you will only * get a stack trace from the point where TraceKit.report was called. * - * Handlers receive a stackInfo object as described in the + * Handlers receive a TraceKit.StackTrace object as described in the * TraceKit.computeStackTrace docs. + * + * @memberof TraceKit + * @namespace */ TraceKit.report = (function reportModuleWrapper() { var handlers = [], + lastArgs = null, lastException = null, lastExceptionStack = null; /** * Add a crash handler. * @param {Function} handler + * @memberof TraceKit.report */ function subscribe(handler) { installGlobalHandler(); @@ -113,6 +132,7 @@ TraceKit.report = (function reportModuleWrapper() { /** * Remove a crash handler. * @param {Function} handler + * @memberof TraceKit.report */ function unsubscribe(handler) { for (var i = handlers.length - 1; i >= 0; --i) { @@ -124,11 +144,14 @@ TraceKit.report = (function reportModuleWrapper() { /** * Dispatch stack information to all handlers. - * @param {Object.} stack + * @param {TraceKit.StackTrace} stack + * @param {boolean} isWindowError Is this a top-level window error? + * @memberof TraceKit.report + * @throws An exception if an error occurs while calling an handler. */ - function notifyHandlers(stack, windowError) { + function notifyHandlers(stack, isWindowError) { var exception = null; - if (windowError && !TraceKit.collectWindowErrors) { + if (isWindowError && !TraceKit.collectWindowErrors) { return; } for (var i in handlers) { @@ -153,34 +176,36 @@ TraceKit.report = (function reportModuleWrapper() { * Supported by Gecko and IE. * @param {string} message Error message. * @param {string} url URL of script that generated the exception. - * @param {(number|string)} lineNo The line number at which the error - * occurred. + * @param {(number|string)} lineNo The line number at which the error occurred. + * @param {(number|string)=} columnNo The column number at which the error occurred. + * @param {Error=} errorObj The actual Error object. + * @memberof TraceKit.report */ - function traceKitWindowOnError(message, url, lineNo) { + function traceKitWindowOnError(message, url, lineNo, columnNo, errorObj) { var stack = null; if (lastExceptionStack) { TraceKit.computeStackTrace.augmentStackTraceWithInitialElement(lastExceptionStack, url, lineNo, message); - stack = lastExceptionStack; - lastExceptionStack = null; - lastException = null; + processLastException(); + } else if (errorObj) { + stack = TraceKit.computeStackTrace(errorObj); + notifyHandlers(stack, true); } else { var location = { - 'url': url, - 'line': lineNo + 'url': url, + 'line': lineNo, + 'column': columnNo }; location.func = TraceKit.computeStackTrace.guessFunctionName(location.url, location.line); location.context = TraceKit.computeStackTrace.gatherContext(location.url, location.line); stack = { - 'mode': 'onerror', - 'message': message, - 'url': document.location.href, - 'stack': [location], - 'useragent': navigator.userAgent + 'mode': 'onerror', + 'message': message, + 'stack': [location] }; - } - notifyHandlers(stack, 'from window.onerror'); + notifyHandlers(stack, true); + } if (_oldOnerrorHandler) { return _oldOnerrorHandler.apply(this, arguments); @@ -189,8 +214,11 @@ TraceKit.report = (function reportModuleWrapper() { return false; } - function installGlobalHandler () - { + /** + * Install a global onerror handler + * @memberof TraceKit.report + */ + function installGlobalHandler () { if (_onErrorHandlerInstalled === true) { return; } @@ -199,26 +227,38 @@ TraceKit.report = (function reportModuleWrapper() { _onErrorHandlerInstalled = true; } + /** + * Process the most recent exception + * @memberof TraceKit.report + */ + function processLastException() { + var _lastExceptionStack = lastExceptionStack, + _lastArgs = lastArgs; + lastArgs = null; + lastExceptionStack = null; + lastException = null; + notifyHandlers.apply(null, [_lastExceptionStack, false].concat(_lastArgs)); + } + /** * Reports an unhandled Error to TraceKit. * @param {Error} ex + * @memberof TraceKit.report + * @throws An exception if an incomplete stack trace is detected (old IE browsers). */ function report(ex) { - var args = _slice.call(arguments, 1); if (lastExceptionStack) { if (lastException === ex) { return; // already caught by an inner catch block, ignore } else { - var s = lastExceptionStack; - lastExceptionStack = null; - lastException = null; - notifyHandlers.apply(null, [s, null].concat(args)); + processLastException(); } } var stack = TraceKit.computeStackTrace(ex); lastExceptionStack = stack; lastException = ex; + lastArgs = _slice.call(arguments, 1); // If the stack trace is incomplete, wait for 2 seconds for // slow slow IE to see if onerror occurs or not before reporting @@ -226,9 +266,7 @@ TraceKit.report = (function reportModuleWrapper() { // stack trace window.setTimeout(function () { if (lastException === ex) { - lastExceptionStack = null; - lastException = null; - notifyHandlers.apply(null, [stack, null].concat(args)); + processLastException(); } }, (stack.incomplete ? 2000 : 0)); @@ -240,22 +278,36 @@ TraceKit.report = (function reportModuleWrapper() { return report; }()); +/** + * An object representing a single stack frame. + * @typedef {Object} StackFrame + * @property {string} url The JavaScript or HTML file URL. + * @property {string} func The function name, or empty for anonymous functions (if guessing did not work). + * @property {string[]?} args The arguments passed to the function, if known. + * @property {number=} line The line number, if known. + * @property {number=} column The column number, if known. + * @property {string[]} context An array of source code lines; the middle element corresponds to the correct line#. + * @memberof TraceKit + */ + +/** + * An object representing a JavaScript stack trace. + * @typedef {Object} StackTrace + * @property {string} name The name of the thrown exception. + * @property {string} message The exception error message. + * @property {TraceKit.StackFrame[]} stack An array of stack frames. + * @property {string} mode 'stack', 'stacktrace', 'multiline', 'callers', 'onerror', or 'failed' -- method used to collect the stack trace. + * @memberof TraceKit + */ + /** * TraceKit.computeStackTrace: cross-browser stack traces in JavaScript * * Syntax: + * ```js * s = TraceKit.computeStackTrace.ofCaller([depth]) * s = TraceKit.computeStackTrace(exception) // consider using TraceKit.report instead (see below) - * Returns: - * s.name - exception name - * s.message - exception message - * s.stack[i].url - JavaScript or HTML file URL - * s.stack[i].func - function name, or empty for anonymous functions (if guessing did not work) - * s.stack[i].args - arguments passed to the function, if known - * s.stack[i].line - line number, if known - * s.stack[i].column - column number, if known - * s.stack[i].context - an array of source code lines; the middle element corresponds to the correct line# - * s.mode - 'stack', 'stacktrace', 'multiline', 'callers', 'onerror', or 'failed' -- method used to collect the stack trace + * ``` * * Supports: * - Firefox: full stack trace with line numbers and unreliable column @@ -294,6 +346,7 @@ TraceKit.report = (function reportModuleWrapper() { * inner function that actually caused the exception). * * Tracing example: + * ```js * function trace(message) { * var stackInfo = TraceKit.computeStackTrace.ofCaller(); * var data = message + "\n"; @@ -306,6 +359,9 @@ TraceKit.report = (function reportModuleWrapper() { * else * alert(data); * } + * ``` + * @memberof TraceKit + * @namespace */ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { var debug = false, @@ -316,20 +372,21 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { * to look up anonymous function names. * @param {string} url URL of source code. * @return {string} Source contents. + * @memberof TraceKit.computeStackTrace */ function loadSource(url) { if (!TraceKit.remoteFetching) { //Only attempt request if remoteFetching is on. return ''; } try { - function getXHR() { + var getXHR = function() { try { return new window.XMLHttpRequest(); } catch (e) { // explicitly bubble up the exception if not found return new window.ActiveXObject('Microsoft.XMLHTTP'); } - } + }; var request = getXHR(); request.open('GET', url, false); @@ -344,13 +401,29 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { * Retrieves source code from the source code cache. * @param {string} url URL of source code. * @return {Array.} Source contents. + * @memberof TraceKit.computeStackTrace */ function getSource(url) { + if (typeof url !== 'string') { + return []; + } + if (!_has(sourceCache, url)) { // URL needs to be able to fetched within the acceptable domain. Otherwise, // cross-domain errors will be triggered. + /* + Regex matches: + 0 - Full Url + 1 - Protocol + 2 - Domain + 3 - Port (Useful for internal applications) + 4 - Path + */ var source = ''; - if (url.indexOf(document.domain) !== -1) { + var domain = ''; + try { domain = window.document.domain; } catch (e) { } + var match = /(.*)\:\/\/([^:\/]+)([:\d]*)\/{0,1}([\s\S]*)/.exec(url); + if (match && match[2] === domain) { source = loadSource(url); } sourceCache[url] = source ? source.split('\n') : []; @@ -366,6 +439,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { * @param {string} url URL of source code. * @param {(string|number)} lineNo Line number in source code. * @return {string} The function name, if discoverable. + * @memberof TraceKit.computeStackTrace */ function guessFunctionName(url, lineNo) { var reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/, @@ -402,6 +476,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { * @param {(string|number)} line Line number in source code to centre * around for context. * @return {?Array.} Lines of source code. + * @memberof TraceKit.computeStackTrace */ function gatherContext(url, line) { var source = getSource(url); @@ -436,6 +511,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { * used inside a regular expression as a string literal. * @param {string} text The string. * @return {string} The escaped string literal. + * @memberof TraceKit.computeStackTrace */ function escapeRegExp(text) { return text.replace(/[\-\[\]{}()*+?.,\\\^$|#]/g, '\\$&'); @@ -447,6 +523,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { * be matched the same as their literal friends. * @param {string} body The string. * @return {string} The escaped string. + * @memberof TraceKit.computeStackTrace */ function escapeCodeAsRegExpForMatchingInsideHTML(body) { return escapeRegExp(body).replace('<', '(?:<|<)').replace('>', '(?:>|>)').replace('&', '(?:&|&)').replace('"', '(?:"|")').replace(/\s+/g, '\\s+'); @@ -458,6 +535,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { * @param {Array.} urls A list of URLs to search. * @return {?Object.} An object containing * the url, line, and column number of the defined function. + * @memberof TraceKit.computeStackTrace */ function findSourceInUrls(re, urls) { var source, m; @@ -489,6 +567,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { * @param {string} url The URL to search. * @param {(string|number)} line The line number to examine. * @return {?number} The column number. + * @memberof TraceKit.computeStackTrace */ function findSourceInLine(fragment, url, line) { var source = getSource(url), @@ -510,10 +589,15 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { * function definition. * @return {?Object.} An object containing * the url, line, and column number of the defined function. + * @memberof TraceKit.computeStackTrace */ function findSourceByFunctionBody(func) { + if (_isUndefined(window && window.document)) { + return; + } + var urls = [window.location.href], - scripts = document.getElementsByTagName('script'), + scripts = window.document.getElementsByTagName('script'), body, code = '' + func, codeRE = /^function(?:\s+([\w$]+))?\s*\(([\w\s,]*)\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/, @@ -587,6 +671,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { // ex.message = qq is not defined // ex.fileName = http://... // ex.lineNumber = 59 + // ex.columnNumber = 69 // ex.stack = ...stack trace... (see the example below) // ex.name = ReferenceError // @@ -611,15 +696,17 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { * Computes stack trace information from the stack property. * Chrome and Gecko use this property. * @param {Error} ex - * @return {?Object.} Stack trace information. + * @return {?TraceKit.StackTrace} Stack trace information. + * @memberof TraceKit.computeStackTrace */ function computeStackTraceFromStackProp(ex) { if (!ex.stack) { return null; } - var chrome = /^\s*at (?:((?:\[object object\])?\S+(?: \[as \S+\])?) )?\(?((?:file|http|https):.*?):(\d+)(?::(\d+))?\)?\s*$/i, - gecko = /^\s*(\S*)(?:\((.*?)\))?@((?:file|http|https).*?):(\d+)(?::(\d+))?\s*$/i, + var chrome = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|webpack|eval).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i, + gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|\[native).*?)(?::(\d+))?(?::(\d+))?\s*$/i, + winjs = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i, lines = ex.stack.split('\n'), stack = [], parts, @@ -627,21 +714,31 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { reference = /^(.*) is undefined$/.exec(ex.message); for (var i = 0, j = lines.length; i < j; ++i) { - if ((parts = gecko.exec(lines[i]))) { + if ((parts = chrome.exec(lines[i]))) { + var isNative = parts[2] && parts[2].indexOf('native') !== -1; element = { - 'url': parts[3], + 'url': !isNative ? parts[2] : null, 'func': parts[1] || UNKNOWN_FUNCTION, - 'args': parts[2] ? parts[2].split(',') : '', - 'line': +parts[4], - 'column': parts[5] ? +parts[5] : null + 'args': isNative ? [parts[2]] : [], + 'line': parts[3] ? +parts[3] : null, + 'column': parts[4] ? +parts[4] : null }; - } else if ((parts = chrome.exec(lines[i]))) { + } else if ( parts = winjs.exec(lines[i]) ) { element = { 'url': parts[2], 'func': parts[1] || UNKNOWN_FUNCTION, + 'args': [], 'line': +parts[3], 'column': parts[4] ? +parts[4] : null }; + } else if ((parts = gecko.exec(lines[i]))) { + element = { + 'url': parts[3], + 'func': parts[1] || UNKNOWN_FUNCTION, + 'args': parts[2] ? parts[2].split(',') : [], + 'line': parts[4] ? +parts[4] : null, + 'column': parts[5] ? +parts[5] : null + }; } else { continue; } @@ -657,51 +754,70 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { stack.push(element); } - if (stack[0] && stack[0].line && !stack[0].column && reference) { - stack[0].column = findSourceInLine(reference[1], stack[0].url, stack[0].line); - } - if (!stack.length) { return null; } + if (stack[0] && stack[0].line && !stack[0].column && reference) { + stack[0].column = findSourceInLine(reference[1], stack[0].url, stack[0].line); + } else if (!stack[0].column && !_isUndefined(ex.columnNumber)) { + // FireFox uses this awesome columnNumber property for its top frame + // Also note, Firefox's column number is 0-based and everything else expects 1-based, + // so adding 1 + stack[0].column = ex.columnNumber + 1; + } + return { 'mode': 'stack', 'name': ex.name, 'message': ex.message, - 'url': document.location.href, - 'stack': stack, - 'useragent': navigator.userAgent + 'stack': stack }; } /** * Computes stack trace information from the stacktrace property. - * Opera 10 uses this property. + * Opera 10+ uses this property. * @param {Error} ex - * @return {?Object.} Stack trace information. + * @return {?TraceKit.StackTrace} Stack trace information. + * @memberof TraceKit.computeStackTrace */ function computeStackTraceFromStacktraceProp(ex) { // Access and store the stacktrace property before doing ANYTHING // else to it because Opera is not very good at providing it // reliably in other circumstances. var stacktrace = ex.stacktrace; + if (!stacktrace) { + return; + } - var testRE = / line (\d+), column (\d+) in (?:]+)>|([^\)]+))\((.*)\) in (.*):\s*$/i, + var opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i, + opera11Regex = / line (\d+), column (\d+)\s*(?:in (?:]+)>|([^\)]+))\((.*)\))? in (.*):\s*$/i, lines = stacktrace.split('\n'), stack = [], parts; - for (var i = 0, j = lines.length; i < j; i += 2) { - if ((parts = testRE.exec(lines[i]))) { - var element = { + for (var line = 0; line < lines.length; line += 2) { + var element = null; + if ((parts = opera10Regex.exec(lines[line]))) { + element = { + 'url': parts[2], + 'line': +parts[1], + 'column': null, + 'func': parts[3], + 'args':[] + }; + } else if ((parts = opera11Regex.exec(lines[line]))) { + element = { + 'url': parts[6], 'line': +parts[1], 'column': +parts[2], 'func': parts[3] || parts[4], - 'args': parts[5] ? parts[5].split(',') : [], - 'url': parts[6] + 'args': parts[5] ? parts[5].split(',') : [] }; + } + if (element) { if (!element.func && element.line) { element.func = guessFunctionName(element.url, element.line); } @@ -712,7 +828,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { } if (!element.context) { - element.context = [lines[i + 1]]; + element.context = [lines[line + 1]]; } stack.push(element); @@ -727,9 +843,7 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { 'mode': 'stacktrace', 'name': ex.name, 'message': ex.message, - 'url': document.location.href, - 'stack': stack, - 'useragent': navigator.userAgent + 'stack': stack }; } @@ -740,9 +854,11 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { * Opera 9 and earlier use this method if the option to show stack * traces is turned on in opera:config. * @param {Error} ex - * @return {?Object.} Stack information. + * @return {?TraceKit.StackTrace} Stack information. + * @memberof TraceKit.computeStackTrace */ function computeStackTraceFromOperaMultiLineMessage(ex) { + // TODO: Clean this function up // Opera includes a stack trace into the exception message. An example is: // // Statement on line 3: Undefined variable: undefinedFunc @@ -762,40 +878,42 @@ TraceKit.computeStackTrace = (function computeStackTraceWrapper() { return null; } - var lineRE1 = /^\s*Line (\d+) of linked script ((?:file|http|https)\S+)(?:: in function (\S+))?\s*$/i, - lineRE2 = /^\s*Line (\d+) of inline#(\d+) script in ((?:file|http|https)\S+)(?:: in function (\S+))?\s*$/i, + var lineRE1 = /^\s*Line (\d+) of linked script ((?:file|https?|blob)\S+)(?:: in function (\S+))?\s*$/i, + lineRE2 = /^\s*Line (\d+) of inline#(\d+) script in ((?:file|https?|blob)\S+)(?:: in function (\S+))?\s*$/i, lineRE3 = /^\s*Line (\d+) of function script\s*$/i, stack = [], - scripts = document.getElementsByTagName('script'), + scripts = (window && window.document && window.document.getElementsByTagName('script')), inlineScriptBlocks = [], - parts, - i, - len, - source; + parts; - for (i in scripts) { - if (_has(scripts, i) && !scripts[i].src) { - inlineScriptBlocks.push(scripts[i]); + for (var s in scripts) { + if (_has(scripts, s) && !scripts[s].src) { + inlineScriptBlocks.push(scripts[s]); } } - for (i = 2, len = lines.length; i < len; i += 2) { + for (var line = 2; line < lines.length; line += 2) { var item = null; - if ((parts = lineRE1.exec(lines[i]))) { + if ((parts = lineRE1.exec(lines[line]))) { item = { 'url': parts[2], 'func': parts[3], - 'line': +parts[1] + 'args': [], + 'line': +parts[1], + 'column': null }; - } else if ((parts = lineRE2.exec(lines[i]))) { + } else if ((parts = lineRE2.exec(lines[line]))) { item = { 'url': parts[3], - 'func': parts[4] + 'func': parts[4], + 'args': [], + 'line': +parts[1], + 'column': null // TODO: Check to see if inline#1 (+parts[2]) points to the script number or column number. }; var relativeLine = (+parts[1]); // relative to the start of the