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