From d1b364eaf08502b8b7d65c124833b617577fd081 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:28:50 -0800 Subject: [PATCH] feat: Vendor TraceKit (#729) This PR vendors tracekit to alleviate some issues with ESM compatibility. Currently the typescript conversion is minimal. It adds the types which are largely compatible and leaves most of it reasonably unchanged. This PR does not port the tests from tracekit to this repository. A subsequent PR will do this. BEGIN_COMMIT_OVERRIDE feat: Vendor TraceKit feat: Export browser-telemetry initialization method. END_COMMIT_OVERRIDE Jira: EMSR-14 Jira: EMSR-15 --- .../telemetry/browser-telemetry/package.json | 3 - .../src/BrowserTelemetryImpl.ts | 10 +- .../telemetry/browser-telemetry/src/index.ts | 10 + .../src/stack/StackParser.ts | 5 +- .../browser-telemetry/src/vendor/TraceKit.ts | 1110 +++++++++++++++++ 5 files changed, 1129 insertions(+), 9 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 4b26088a1e..d7180ec0aa 100644 --- a/packages/telemetry/browser-telemetry/package.json +++ b/packages/telemetry/browser-telemetry/package.json @@ -43,9 +43,6 @@ "bugs": { "url": "https://github.com/launchdarkly/js-core/issues" }, - "dependencies": { - "tracekit": "^0.4.6" - }, "devDependencies": { "@jest/globals": "^29.7.0", "@launchdarkly/js-client-sdk": "0.3.2", diff --git a/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts b/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts index 9e030dbe93..6de65b1ed6 100644 --- a/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts +++ b/packages/telemetry/browser-telemetry/src/BrowserTelemetryImpl.ts @@ -1,5 +1,3 @@ -import * as TraceKit from 'tracekit'; - /** * A limited selection of type information is provided by the browser client SDK. * This is only a type dependency and these types should be compatible between @@ -23,6 +21,7 @@ import makeInspectors from './inspectors'; import { ParsedOptions, ParsedStackOptions } from './options'; import randomUuidV4 from './randomUuidV4'; import parse from './stack/StackParser'; +import { getTraceKit } from './vendor/TraceKit'; // TODO: Use a ring buffer for the breadcrumbs/pending events instead of shifting. (SDK-914) @@ -54,13 +53,18 @@ 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 // 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. + const 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..c2c50a935b 100644 --- a/packages/telemetry/browser-telemetry/src/index.ts +++ b/packages/telemetry/browser-telemetry/src/index.ts @@ -1 +1,11 @@ +import { BrowserTelemetry } from './api/BrowserTelemetry'; +import { Options } from './api/Options'; +import BrowserTelemetryImpl from './BrowserTelemetryImpl'; +import parse from './options'; + export * from './api'; + +export function initializeTelemetry(options?: Options): BrowserTelemetry { + const parsedOptions = parse(options || {}); + return new BrowserTelemetryImpl(parsedOptions); +} diff --git a/packages/telemetry/browser-telemetry/src/stack/StackParser.ts b/packages/telemetry/browser-telemetry/src/stack/StackParser.ts index 89b88ab869..73f7552946 100644 --- a/packages/telemetry/browser-telemetry/src/stack/StackParser.ts +++ b/packages/telemetry/browser-telemetry/src/stack/StackParser.ts @@ -1,8 +1,7 @@ -import { computeStackTrace } from 'tracekit'; - import { StackFrame } from '../api/stack/StackFrame'; import { StackTrace } from '../api/stack/StackTrace'; import { ParsedStackOptions } from '../options'; +import { getTraceKit } from '../vendor/TraceKit'; /** * In the browser we will not always be able to determine the source file that code originates @@ -195,7 +194,7 @@ 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 parsed = getTraceKit().computeStackTrace(error); const frames: StackFrame[] = parsed.stack.reverse().map((inFrame) => ({ fileName: processUrlToFileName(inFrame.url, window.location.origin), function: inFrame.func, 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..a832ddf224 --- /dev/null +++ b/packages/telemetry/browser-telemetry/src/vendor/TraceKit.ts @@ -0,0 +1,1110 @@ +/** + * https://github.com/csnover/TraceKit + * @license MIT + * @namespace TraceKit + */ + +/** + * This file has been vendored to make it compatible with ESM and to any potential window + * level TraceKit instance. + * + * Functionality unused by this SDK has been removed to minimize size. + * + * It has additionally been converted to typescript. + */ + +/** + * Currently the conversion to typescript is minimal, so the following eslint + * rules are disabled. + */ + +/* eslint-disable func-names */ +/* eslint-disable no-shadow-restricted-names */ +/* eslint-disable prefer-destructuring */ +/* eslint-disable no-param-reassign */ +/* eslint-disable no-cond-assign */ +/* eslint-disable consistent-return */ +/* eslint-disable no-empty */ +/* eslint-disable no-plusplus */ +/* eslint-disable prefer-rest-params */ +/* eslint-disable no-useless-escape */ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-use-before-define */ +/* eslint-disable no-continue */ +/* eslint-disable no-underscore-dangle */ + +export interface TraceKitStatic { + computeStackTrace: { + (ex: Error, depth?: number): StackTrace; + augmentStackTraceWithInitialElement: ( + stackInfo: StackTrace, + url: string, + lineNo: number | string, + message: string, + ) => boolean; + computeStackTraceFromStackProp: (ex: Error) => StackTrace | null; + guessFunctionName: (url: string, lineNo: number | string) => string; + gatherContext: (url: string, line: number | string) => string[] | null; + ofCaller: (depth?: number) => StackTrace; + getSource: (url: string) => string[]; + }; + remoteFetching: boolean; + collectWindowErrors: boolean; + linesOfContext: number; + debug: boolean; +} + +const TraceKit: any = {}; + +export interface StackFrame { + url: string; + func: string; + args?: string[]; + line?: number; + column?: number; + context?: string[]; +} + +export type Mode = 'stack' | 'stacktrace' | 'multiline' | 'callers' | 'onerror' | 'failed'; + +export interface StackTrace { + name: string; + message: string; + stack: StackFrame[]; + mode: Mode; + incomplete?: boolean; + partial?: boolean; +} + +(function (window, undefined) { + if (!window) { + return; + } + + // global reference to slice + const _slice = [].slice; + const UNKNOWN_FUNCTION = '?'; + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Error_types + const 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: any, key: string): boolean { + 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: any): boolean { + return typeof what === 'undefined'; + } + + /** + * 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): Function { + function wrapped(this: any) { + try { + return func.apply(this, arguments); + } catch (e) { + TraceKit.report(e); + throw e; + } + } + return wrapped; + }; + + /** + * 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() { + const debug = false; + const sourceCache: Record = {}; + + /** + * 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: string): string { + if (!TraceKit.remoteFetching) { + // Only attempt request if remoteFetching is on. + return ''; + } + try { + const getXHR = function () { + try { + return new window.XMLHttpRequest(); + } catch (e) { + // explicitly bubble up the exception if not found + // @ts-ignore + return new window.ActiveXObject('Microsoft.XMLHTTP'); + } + }; + + const 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: string): string[] { + 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 + */ + let source = ''; + let domain = ''; + try { + domain = window.document.domain; + } catch (e) {} + const 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: string, lineNo: string | number) { + if (typeof lineNo !== 'number') { + lineNo = Number(lineNo); + } + const reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/; + const reGuessFunction = /['"]?([0-9A-Za-z$_]+)['"]?\s*[:=]\s*(function|eval|new Function)/; + let line = ''; + const maxLines = 10; + const source = getSource(url); + let 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 (let i = 0; i < maxLines; ++i) { + line = source[lineNo - i] + line; + + if (!_isUndefined(line)) { + if ((m = reGuessFunction.exec(line))) { + return m[1]; + } + 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: string, line: string | number): string[] | null { + if (typeof line !== 'number') { + line = Number(line); + } + const source = getSource(url); + + if (!source.length) { + return null; + } + + const context = []; + // linesBefore & linesAfter are inclusive with the offending line. + // if linesOfContext is even, there will be one extra line + // *before* the offending line. + const linesBefore = Math.floor(TraceKit.linesOfContext / 2); + // Add one extra line if linesOfContext is odd + const linesAfter = linesBefore + (TraceKit.linesOfContext % 2); + const start = Math.max(0, line - linesBefore - 1); + const end = Math.min(source.length, line + linesAfter - 1); + + line -= 1; // convert to 0-based index + + for (let 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: string): string { + 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: string): string { + 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: RegExp, + urls: string[], + ): { + url: string; + line: number; + column: number; + } | null { + let source: any; + let m: any; + for (let 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: string, url: string, line: string | number): number | null { + if (typeof line !== 'number') { + line = Number(line); + } + const source = getSource(url); + const re = new RegExp(`\\b${escapeRegExp(fragment)}\\b`); + let m: any; + + 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: Function | string) { + if (_isUndefined(window && window.document)) { + return; + } + + const urls = [window.location.href]; + const scripts = window.document.getElementsByTagName('script'); + let body; + const code = `${func}`; + const codeRE = /^function(?:\s+([\w$]+))?\s*\(([\w\s,]*)\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/; + const eventRE = /^function on([\w$]+)\s*\(event\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/; + let re; + let parts; + let result; + + for (let i = 0; i < scripts.length; ++i) { + const 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 { + const name = parts[1] ? `\\s+${parts[1]}` : ''; + const 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))) { + const 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'); + + // The below line is as it appears in the original code. + // @ts-expect-error TODO (SDK-1037): Determine if this is a bug or handling for some unexpected case. + 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: any): StackTrace | null { + if (!ex.stack) { + return null; + } + + const chrome = + /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack||\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i; + const gecko = + /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i; + const 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 + let isEval; + const geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i; + const chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/; + + const lines = ex.stack.split('\n'); + const stack: any = []; + let submatch: any; + let parts: any; + let element: any; + const reference: any = /^(.*) is undefined$/.exec(ex.message); + + for (let i = 0, j = lines.length; i < j; ++i) { + if ((parts = chrome.exec(lines[i]))) { + const 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, + }; + } + + /** + * 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: any): StackTrace | null { + // 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. + const { stacktrace } = ex; + if (!stacktrace) { + return null; + } + + const opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i; + const opera11Regex = + / line (\d+), column (\d+)\s*(?:in (?:]+)>|([^\)]+))\((.*)\))? in (.*):\s*$/i; + const lines = stacktrace.split('\n'); + const stack = []; + let parts; + + for (let line = 0; line < lines.length; line += 2) { + let element: any = 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, + }; + } + + /** + * 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: Error): StackTrace | null { + // 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); } + // ... + + const lines = ex.message.split('\n'); + if (lines.length < 4) { + return null; + } + + const lineRE1 = + /^\s*Line (\d+) of linked script ((?:file|https?|blob)\S+)(?:: in function (\S+))?\s*$/i; + const lineRE2 = + /^\s*Line (\d+) of inline#(\d+) script in ((?:file|https?|blob)\S+)(?:: in function (\S+))?\s*$/i; + const lineRE3 = /^\s*Line (\d+) of function script\s*$/i; + const stack = []; + const scripts = window && window.document && window.document.getElementsByTagName('script'); + const inlineScriptBlocks = []; + let parts: any; + + for (const s in scripts) { + if (_has(scripts, s) && !scripts[s].src) { + inlineScriptBlocks.push(scripts[s]); + } + } + + for (let line = 2; line < lines.length; line += 2) { + let item: any = 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. + }; + const relativeLine = +parts[1]; // relative to the start of the