From 081eb20789458d93e5e8d708cac5fa36bb6d4f3a Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 13 Jan 2025 08:29:49 -0800 Subject: [PATCH 1/3] feat: Export browser-telemetry initialization method. --- .../browser-telemetry/src/BrowserTelemetryImpl.ts | 6 +++++- packages/telemetry/browser-telemetry/src/index.ts | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts b/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts index 41ce9a3510..543810e1a0 100644 --- a/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts +++ b/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts @@ -60,7 +60,11 @@ function configureTraceKit(options: ParsedStackOptions) { // from the before context. // The typing for this is a bool, but it accepts a number. const beforeAfterMax = Math.max(options.source.afterLines, options.source.beforeLines); - (TraceKit as any).linesOfContext = beforeAfterMax * 2 + 1; + // The assignment here has bene split to prevent esbuild from complaining about an assigment to + // an import. TraceKit exports a single object and the interface requires modifying an exported + // var. + let anyObj = TraceKit as any; + anyObj.linesOfContext = beforeAfterMax * 2 + 1; } export default class BrowserTelemetryImpl implements BrowserTelemetry { diff --git a/packages/telemetry/browser-telemetry/src/index.ts b/packages/telemetry/browser-telemetry/src/index.ts index b1c13e7340..0c38f67ab5 100644 --- a/packages/telemetry/browser-telemetry/src/index.ts +++ b/packages/telemetry/browser-telemetry/src/index.ts @@ -1 +1,11 @@ export * from './api'; + +import { BrowserTelemetry } from './api/BrowserTelemetry'; +import { Options } from './api/Options'; +import BrowserTelemetryImpl from './BrowserTelemetryImpl'; +import parse from './options'; + +export function initializeTelemetry(options?: Options): BrowserTelemetry { + const parsedOptions = parse(options || {}); + return new BrowserTelemetryImpl(parsedOptions); +} From 9ffe92af118f62c11a68a60461f08e2ce58b8461 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 14 Jan 2025 09:06:22 -0800 Subject: [PATCH 2/3] feat: Vendor tacekit. --- .../telemetry/browser-telemetry/package.json | 2 +- .../src/BrowserTelemetryImpl.ts | 3 +- .../src/stack/StackParser.ts | 7 +- .../browser-telemetry/src/vendor/TraceKit.ts | 1321 +++++++++++++++++ 4 files changed, 1328 insertions(+), 5 deletions(-) create mode 100644 packages/telemetry/browser-telemetry/src/vendor/TraceKit.ts diff --git a/packages/telemetry/browser-telemetry/package.json b/packages/telemetry/browser-telemetry/package.json index 2b35364212..10ce7a5b91 100644 --- a/packages/telemetry/browser-telemetry/package.json +++ b/packages/telemetry/browser-telemetry/package.json @@ -45,7 +45,7 @@ }, "dependencies": { "rrweb": "2.0.0-alpha.4", - "tracekit": "^0.4.6" + "tracekit": "0.4.6" }, "devDependencies": { "@jest/globals": "^29.7.0", diff --git a/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts b/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts index 543810e1a0..36c022d584 100644 --- a/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts +++ b/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts @@ -1,4 +1,4 @@ -import * as TraceKit from 'tracekit'; +import {getTraceKit} from './vendor/TraceKit'; /** * A limited selection of type information is provided by the browser client SDK. @@ -54,6 +54,7 @@ function safeValue(u: unknown): string | boolean | number | undefined { } function configureTraceKit(options: ParsedStackOptions) { + const TraceKit = getTraceKit(); // Include before + after + source line. // TraceKit only takes a total context size, so we have to over capture and then reduce the lines. // So, for instance if before is 3 and after is 4 we need to capture 4 and 4 and then drop a line diff --git a/packages/telemetry/browser-telemetry/src/stack/StackParser.ts b/packages/telemetry/browser-telemetry/src/stack/StackParser.ts index 89b88ab869..cc4eb0d6b3 100644 --- a/packages/telemetry/browser-telemetry/src/stack/StackParser.ts +++ b/packages/telemetry/browser-telemetry/src/stack/StackParser.ts @@ -1,4 +1,4 @@ -import { computeStackTrace } from 'tracekit'; +import { getTraceKit } from '../vendor/TraceKit'; import { StackFrame } from '../api/stack/StackFrame'; import { StackTrace } from '../api/stack/StackTrace'; @@ -195,8 +195,9 @@ export function getSrcLines( * @returns The stack trace for the given error. */ export default function parse(error: Error, options: ParsedStackOptions): StackTrace { - const parsed = computeStackTrace(error); - const frames: StackFrame[] = parsed.stack.reverse().map((inFrame) => ({ + // @ts-ignore + const parsed = getTraceKit().computeStackTrace(error); + const frames: StackFrame[] = parsed.stack.reverse().map((inFrame: any) => ({ fileName: processUrlToFileName(inFrame.url, window.location.origin), function: inFrame.func, line: inFrame.line, diff --git a/packages/telemetry/browser-telemetry/src/vendor/TraceKit.ts b/packages/telemetry/browser-telemetry/src/vendor/TraceKit.ts new file mode 100644 index 0000000000..00fc5be1a3 --- /dev/null +++ b/packages/telemetry/browser-telemetry/src/vendor/TraceKit.ts @@ -0,0 +1,1321 @@ +let TraceKit = {}; + +/** + * https://github.com/csnover/TraceKit + * @license MIT + * @namespace TraceKit + */ +(function(window, undefined) { + if (!window) { + return; + } + + // global reference to slice + var _slice = [].slice; + var UNKNOWN_FUNCTION = '?'; + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Error_types + var ERROR_TYPES_RE = /^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Range|Reference|Syntax|Type|URI|)Error): )?(.*)$/; + + /** + * A better form of hasOwnProperty
+ * Example: `_has(MainHostObject, property) === true/false` + * + * @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'; + } + + /** + * Export TraceKit out to another variable
+ * Example: `var TK = TraceKit.noConflict()` + * @return {Object} The TraceKit object + * @memberof TraceKit + */ + TraceKit.noConflict = function noConflict() { + return TraceKit; + }; + + /** + * 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() { + try { + return func.apply(this, arguments); + } catch (e) { + TraceKit.report(e); + throw e; + } + } + return wrapped; + }; + + /** + * 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 + * + * In theory, TraceKit should work on all of the following versions: + * - IE5.5+ (only 8.0 tested) + * - Firefox 0.9+ (only 3.5+ tested) + * - Opera 7+ (only 10.50 tested; versions 9 and earlier may require + * Exceptions Have Stacktrace to be enabled in opera:config) + * - Safari 3+ (only 4+ tested) + * - Chrome 1+ (only 5+ tested) + * - Konqueror 3.5+ (untested) + * + * Requires TraceKit.computeStackTrace. + * + * Tries to catch all unhandled exceptions and report them to the + * subscribed handlers. Please note that TraceKit.report will rethrow the + * exception. This is REQUIRED in order to get a useful stack trace in IE. + * 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 TraceKit.StackTrace object as described in the + * TraceKit.computeStackTrace docs. + * + * @memberof TraceKit + * @namespace + */ + TraceKit.report = (function reportModuleWrapper() { + var handlers = [], + lastException = null, + lastExceptionStack = null; + + /** + * Add a crash handler. + * @param {Function} handler + * @memberof TraceKit.report + */ + function subscribe(handler) { + installGlobalHandler(); + installGlobalUnhandledRejectionHandler(); + handlers.push(handler); + } + + /** + * Remove a crash handler. + * @param {Function} handler + * @memberof TraceKit.report + */ + function unsubscribe(handler) { + for (var i = handlers.length - 1; i >= 0; --i) { + if (handlers[i] === handler) { + handlers.splice(i, 1); + } + } + + if (handlers.length === 0) { + uninstallGlobalHandler(); + uninstallGlobalUnhandledRejectionHandler(); + } + } + + /** + * Dispatch stack information to all handlers. + * @param {TraceKit.StackTrace} stack + * @param {boolean} isWindowError Is this a top-level window error? + * @param {Error=} error The error that's being handled (if available, null otherwise) + * @memberof TraceKit.report + * @throws An exception if an error occurs while calling an handler. + */ + function notifyHandlers(stack, isWindowError, error) { + var exception = null; + if (isWindowError && !TraceKit.collectWindowErrors) { + return; + } + for (var i in handlers) { + if (_has(handlers, i)) { + try { + handlers[i](stack, isWindowError, error); + } catch (inner) { + exception = inner; + } + } + } + + if (exception) { + throw exception; + } + } + + var _oldOnerrorHandler, _onErrorHandlerInstalled; + var _oldOnunhandledrejectionHandler, _onUnhandledRejectionHandlerInstalled; + + /** + * Ensures all global unhandled exceptions are recorded. + * 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)=} columnNo The column number at which the error occurred. + * @param {Error=} errorObj The actual Error object. + * @memberof TraceKit.report + */ + function traceKitWindowOnError(message, url, lineNo, columnNo, errorObj) { + var stack = null; + + if (lastExceptionStack) { + TraceKit.computeStackTrace.augmentStackTraceWithInitialElement(lastExceptionStack, url, lineNo, message); + processLastException(); + } else if (errorObj) { + stack = TraceKit.computeStackTrace(errorObj); + notifyHandlers(stack, true, errorObj); + } else { + var location = { + 'url': url, + 'line': lineNo, + 'column': columnNo + }; + + var name; + var msg = message; // must be new var or will modify original `arguments` + if ({}.toString.call(message) === '[object String]') { + var groups = message.match(ERROR_TYPES_RE); + if (groups) { + name = groups[1]; + msg = groups[2]; + } + } + + location.func = TraceKit.computeStackTrace.guessFunctionName(location.url, location.line); + location.context = TraceKit.computeStackTrace.gatherContext(location.url, location.line); + stack = { + 'name': name, + 'message': msg, + 'mode': 'onerror', + 'stack': [location] + }; + + notifyHandlers(stack, true, null); + } + + if (_oldOnerrorHandler) { + return _oldOnerrorHandler.apply(this, arguments); + } + + return false; + } + + /** + * Ensures all unhandled rejections are recorded. + * @param {PromiseRejectionEvent} e event. + * @memberof TraceKit.report + * @see https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onunhandledrejection + * @see https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent + */ + function traceKitWindowOnUnhandledRejection(e) { + var stack = TraceKit.computeStackTrace(e.reason); + notifyHandlers(stack, true, e.reason); + } + + /** + * Install a global onerror handler + * @memberof TraceKit.report + */ + function installGlobalHandler() { + if (_onErrorHandlerInstalled === true) { + return; + } + + _oldOnerrorHandler = window.onerror; + window.onerror = traceKitWindowOnError; + _onErrorHandlerInstalled = true; + } + + /** + * Uninstall the global onerror handler + * @memberof TraceKit.report + */ + function uninstallGlobalHandler() { + if (_onErrorHandlerInstalled) { + window.onerror = _oldOnerrorHandler; + _onErrorHandlerInstalled = false; + } + } + + /** + * Install a global onunhandledrejection handler + * @memberof TraceKit.report + */ + function installGlobalUnhandledRejectionHandler() { + if (_onUnhandledRejectionHandlerInstalled === true) { + return; + } + + _oldOnunhandledrejectionHandler = window.onunhandledrejection; + window.onunhandledrejection = traceKitWindowOnUnhandledRejection; + _onUnhandledRejectionHandlerInstalled = true; + } + + /** + * Uninstall the global onunhandledrejection handler + * @memberof TraceKit.report + */ + function uninstallGlobalUnhandledRejectionHandler() { + if (_onUnhandledRejectionHandlerInstalled) { + window.onunhandledrejection = _oldOnunhandledrejectionHandler; + _onUnhandledRejectionHandlerInstalled = false; + } + } + + /** + * Process the most recent exception + * @memberof TraceKit.report + */ + function processLastException() { + var _lastExceptionStack = lastExceptionStack, + _lastException = lastException; + lastExceptionStack = null; + lastException = null; + notifyHandlers(_lastExceptionStack, false, _lastException); + } + + /** + * 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) { + if (lastExceptionStack) { + if (lastException === ex) { + return; // already caught by an inner catch block, ignore + } else { + processLastException(); + } + } + + var stack = TraceKit.computeStackTrace(ex); + lastExceptionStack = stack; + lastException = ex; + + // If the stack trace is incomplete, wait for 2 seconds for + // slow slow IE to see if onerror occurs or not before reporting + // this exception; otherwise, we will end up with an incomplete + // stack trace + setTimeout(function () { + if (lastException === ex) { + processLastException(); + } + }, (stack.incomplete ? 2000 : 0)); + + throw ex; // re-throw to propagate to the top level (and cause window.onerror) + } + + report.subscribe = subscribe; + report.unsubscribe = unsubscribe; + 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) + * ``` + * + * Supports: + * - Firefox: full stack trace with line numbers and unreliable column + * number on top frame + * - Opera 10: full stack trace with line and column numbers + * - Opera 9-: full stack trace with line numbers + * - Chrome: full stack trace with line and column numbers + * - Safari: line and column number for the topmost stacktrace element + * only + * - IE: no line numbers whatsoever + * + * Tries to guess names of anonymous functions by looking for assignments + * in the source code. In IE and Safari, we have to guess source file names + * by searching for function bodies inside all page scripts. This will not + * work for scripts that are loaded cross-domain. + * Here be dragons: some function names may be guessed incorrectly, and + * duplicate functions may be mismatched. + * + * TraceKit.computeStackTrace should only be used for tracing purposes. + * Logging of unhandled exceptions should be done with TraceKit.report, + * which builds on top of TraceKit.computeStackTrace and provides better + * IE support by utilizing the window.onerror event to retrieve information + * about the top of the stack. + * + * Note: In IE and Safari, no stack trace is recorded on the Error object, + * so computeStackTrace instead walks its *own* chain of callers. + * This means that: + * * in Safari, some methods may be missing from the stack trace; + * * in IE, the topmost function in the stack trace will always be the + * caller of computeStackTrace. + * + * This is okay for tracing (because you are likely to be calling + * computeStackTrace from the function you want to be the topmost element + * of the stack trace anyway), but not okay for logging unhandled + * exceptions (because your catch block will likely be far away from the + * inner function that actually caused the exception). + * + * Tracing example: + * ```js + * function trace(message) { + * var stackInfo = TraceKit.computeStackTrace.ofCaller(); + * var data = message + "\n"; + * for(var i in stackInfo.stack) { + * var item = stackInfo.stack[i]; + * data += (item.func || '[anonymous]') + "() in " + item.url + ":" + (item.line || '0') + "\n"; + * } + * if (window.console) + * console.info(data); + * else + * alert(data); + * } + * ``` + * @memberof TraceKit + * @namespace + */ + TraceKit.computeStackTrace = (function computeStackTraceWrapper() { + var debug = false, + sourceCache = {}; + + /** + * Attempts to retrieve source code via XMLHttpRequest, which is used + * 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 { + 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); + request.send(''); + return request.responseText; + } catch (e) { + return ''; + } + } + + /** + * 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 = ''; + 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') : []; + } + + return sourceCache[url]; + } + + /** + * Tries to use an externally loaded copy of source code to determine + * the name of a function by looking at the name of the variable it was + * assigned to, if any. + * @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 ([^(]*)\(([^)]*)\)/, + reGuessFunction = /['"]?([0-9A-Za-z$_]+)['"]?\s*[:=]\s*(function|eval|new Function)/, + line = '', + maxLines = 10, + source = getSource(url), + m; + + if (!source.length) { + return UNKNOWN_FUNCTION; + } + + // Walk backwards from the first line in the function until we find the line which + // matches the pattern above, which is the function definition + for (var i = 0; i < maxLines; ++i) { + line = source[lineNo - i] + line; + + if (!_isUndefined(line)) { + if ((m = reGuessFunction.exec(line))) { + return m[1]; + } else if ((m = reFunctionArgNames.exec(line))) { + return m[1]; + } + } + } + + return UNKNOWN_FUNCTION; + } + + /** + * Retrieves the surrounding lines from where an exception occurred. + * @param {string} url URL of source code. + * @param {(string|number)} line Line number in source code to center around for context. + * @return {?Array.} Lines of source code. + * @memberof TraceKit.computeStackTrace + */ + function gatherContext(url, line) { + var source = getSource(url); + + if (!source.length) { + return null; + } + + var context = [], + // linesBefore & linesAfter are inclusive with the offending line. + // if linesOfContext is even, there will be one extra line + // *before* the offending line. + linesBefore = Math.floor(TraceKit.linesOfContext / 2), + // Add one extra line if linesOfContext is odd + linesAfter = linesBefore + (TraceKit.linesOfContext % 2), + start = Math.max(0, line - linesBefore - 1), + end = Math.min(source.length, line + linesAfter - 1); + + line -= 1; // convert to 0-based index + + for (var i = start; i < end; ++i) { + if (!_isUndefined(source[i])) { + context.push(source[i]); + } + } + + return context.length > 0 ? context : null; + } + + /** + * Escapes special characters, except for whitespace, in a string to be + * 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, '\\$&'); + } + + /** + * Escapes special characters in a string to be used inside a regular + * expression as a string literal. Also ensures that HTML entities will + * 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+'); + } + + /** + * Determines where a code fragment occurs in the source code. + * @param {RegExp} re The function definition. + * @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; + for (var i = 0, j = urls.length; i < j; ++i) { + if ((source = getSource(urls[i])).length) { + source = source.join('\n'); + if ((m = re.exec(source))) { + + return { + 'url': urls[i], + 'line': source.substring(0, m.index).split('\n').length, + 'column': m.index - source.lastIndexOf('\n', m.index) - 1 + }; + } + } + } + + return null; + } + + /** + * Determines at which column a code fragment occurs on a line of the + * source code. + * @param {string} fragment The code fragment. + * @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), + re = new RegExp('\\b' + escapeRegExp(fragment) + '\\b'), + m; + + line -= 1; + + if (source && source.length > line && (m = re.exec(source[line]))) { + return m.index; + } + + return null; + } + + /** + * Determines where a function was defined within the source code. + * @param {(Function|string)} func A function reference or serialized + * 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 = window.document.getElementsByTagName('script'), + body, + code = '' + func, + codeRE = /^function(?:\s+([\w$]+))?\s*\(([\w\s,]*)\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/, + eventRE = /^function on([\w$]+)\s*\(event\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/, + re, + parts, + result; + + for (var i = 0; i < scripts.length; ++i) { + var script = scripts[i]; + if (script.src) { + urls.push(script.src); + } + } + + if (!(parts = codeRE.exec(code))) { + re = new RegExp(escapeRegExp(code).replace(/\s+/g, '\\s+')); + } + + // not sure if this is really necessary, but I don’t have a test + // corpus large enough to confirm that and it was in the original. + else { + var name = parts[1] ? '\\s+' + parts[1] : '', + args = parts[2].split(',').join('\\s*,\\s*'); + + body = escapeRegExp(parts[3]).replace(/;$/, ';?'); // semicolon is inserted if the function ends with a comment.replace(/\s+/g, '\\s+'); + re = new RegExp('function' + name + '\\s*\\(\\s*' + args + '\\s*\\)\\s*{\\s*' + body + '\\s*}'); + } + + // look for a normal function definition + if ((result = findSourceInUrls(re, urls))) { + return result; + } + + // look for an old-school event handler function + if ((parts = eventRE.exec(code))) { + var event = parts[1]; + body = escapeCodeAsRegExpForMatchingInsideHTML(parts[2]); + + // look for a function defined in HTML as an onXXX handler + re = new RegExp('on' + event + '=[\\\'"]\\s*' + body + '\\s*[\\\'"]', 'i'); + + if ((result = findSourceInUrls(re, urls[0]))) { + return result; + } + + // look for ??? + re = new RegExp(body); + + if ((result = findSourceInUrls(re, urls))) { + return result; + } + } + + return null; + } + + // Contents of Exception in various browsers. + // + // SAFARI: + // ex.message = Can't find variable: qq + // ex.line = 59 + // ex.sourceId = 580238192 + // ex.sourceURL = http://... + // ex.expressionBeginOffset = 96 + // ex.expressionCaretOffset = 98 + // ex.expressionEndOffset = 98 + // ex.name = ReferenceError + // + // FIREFOX: + // 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 + // + // CHROME: + // ex.message = qq is not defined + // ex.name = ReferenceError + // ex.type = not_defined + // ex.arguments = ['aa'] + // ex.stack = ...stack trace... + // + // INTERNET EXPLORER: + // ex.message = ... + // ex.name = ReferenceError + // + // OPERA: + // ex.message = ...message... (see the example below) + // ex.name = ReferenceError + // ex.opera#sourceloc = 11 (pretty much useless, duplicates the info in ex.message) + // ex.stacktrace = n/a; see 'opera:config#UserPrefs|Exceptions Have Stacktrace' + + /** + * Computes stack trace information from the stack property. + * Chrome and Gecko use this property. + * @param {Error} ex + * @return {?TraceKit.StackTrace} Stack trace information. + * @memberof TraceKit.computeStackTrace + */ + function computeStackTraceFromStackProp(ex) { + if (!ex.stack) { + return null; + } + + var chrome = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack||\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i, + gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i, + winjs = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i, + + // Used to additionally parse URL/line/column from eval frames + isEval, + geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i, + chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/, + + lines = ex.stack.split('\n'), + stack = [], + submatch, + parts, + element, + reference = /^(.*) is undefined$/.exec(ex.message); + + for (var i = 0, j = lines.length; i < j; ++i) { + if ((parts = chrome.exec(lines[i]))) { + var isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line + isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line + if (isEval && (submatch = chromeEval.exec(parts[2]))) { + // throw out eval line/column and use top-most line/column number + parts[2] = submatch[1]; // url + parts[3] = submatch[2]; // line + parts[4] = submatch[3]; // column + } + element = { + 'url': !isNative ? parts[2] : null, + 'func': parts[1] || UNKNOWN_FUNCTION, + 'args': isNative ? [parts[2]] : [], + 'line': parts[3] ? +parts[3] : null, + 'column': parts[4] ? +parts[4] : null + }; + } 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]))) { + isEval = parts[3] && parts[3].indexOf(' > eval') > -1; + if (isEval && (submatch = geckoEval.exec(parts[3]))) { + // throw out eval line/column and use top-most line number + parts[3] = submatch[1]; + parts[4] = submatch[2]; + parts[5] = null; // no column when eval + } else if (i === 0 && !parts[5] && !_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 + // NOTE: this hack doesn't work if top-most frame is eval + stack[0].column = ex.columnNumber + 1; + } + 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; + } + + if (!element.func && element.line) { + element.func = guessFunctionName(element.url, element.line); + } + + element.context = element.line ? gatherContext(element.url, element.line) : null; + stack.push(element); + } + + 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); + } + + return { + 'mode': 'stack', + 'name': ex.name, + 'message': ex.message, + 'stack': stack + }; + } + + /** + * Computes stack trace information from the stacktrace property. + * Opera 10+ uses this property. + * @param {Error} ex + * @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 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 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(',') : [] + }; + } + + if (element) { + if (!element.func && element.line) { + element.func = guessFunctionName(element.url, element.line); + } + if (element.line) { + try { + element.context = gatherContext(element.url, element.line); + } catch (exc) {} + } + + if (!element.context) { + element.context = [lines[line + 1]]; + } + + stack.push(element); + } + } + + if (!stack.length) { + return null; + } + + return { + 'mode': 'stacktrace', + 'name': ex.name, + 'message': ex.message, + 'stack': stack + }; + } + + /** + * NOT TESTED. + * Computes stack trace information from an error message that includes + * the stack trace. + * Opera 9 and earlier use this method if the option to show stack + * traces is turned on in opera:config. + * @param {Error} ex + * @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 + // Backtrace: + // Line 3 of linked script file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.js: In function zzz + // undefinedFunc(a); + // Line 7 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html: In function yyy + // zzz(x, y, z); + // Line 3 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html: In function xxx + // yyy(a, a, a); + // Line 1 of function script + // try { xxx('hi'); return false; } catch(ex) { TraceKit.report(ex); } + // ... + + var lines = ex.message.split('\n'); + if (lines.length < 4) { + return null; + } + + 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 = (window && window.document && window.document.getElementsByTagName('script')), + inlineScriptBlocks = [], + parts; + + for (var s in scripts) { + if (_has(scripts, s) && !scripts[s].src) { + inlineScriptBlocks.push(scripts[s]); + } + } + + for (var line = 2; line < lines.length; line += 2) { + var item = null; + if ((parts = lineRE1.exec(lines[line]))) { + item = { + 'url': parts[2], + 'func': parts[3], + 'args': [], + 'line': +parts[1], + 'column': null + }; + } else if ((parts = lineRE2.exec(lines[line]))) { + item = { + 'url': parts[3], + '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