UNPKG

@posthog/rrweb-plugin-console-record

Version:

Please refer to the [console recipe](../../../docs/recipes/console.md) on how to use this plugin. See the [guide](../../../guide.md) for more info on rrweb.

487 lines (486 loc) 14.9 kB
"use strict"; var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); function patch(source, name, replacement) { try { if (!(name in source)) { return () => { }; } const original = source[name]; const wrapped = replacement(original); if (typeof wrapped === "function") { wrapped.prototype = wrapped.prototype || {}; Object.defineProperties(wrapped, { __rrweb_original__: { enumerable: false, value: original } }); } source[name] = wrapped; return () => { source[name] = original; }; } catch { return () => { }; } } class StackFrame { constructor(obj) { __publicField(this, "fileName"); __publicField(this, "functionName"); __publicField(this, "lineNumber"); __publicField(this, "columnNumber"); this.fileName = obj.fileName || ""; this.functionName = obj.functionName || ""; this.lineNumber = obj.lineNumber; this.columnNumber = obj.columnNumber; } toString() { const lineNumber = this.lineNumber || ""; const columnNumber = this.columnNumber || ""; if (this.functionName) return `${this.functionName} (${this.fileName}:${lineNumber}:${columnNumber})`; return `${this.fileName}:${lineNumber}:${columnNumber}`; } } const FIREFOX_SAFARI_STACK_REGEXP = /(^|@)\S+:\d+/; const CHROME_IE_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m; const SAFARI_NATIVE_CODE_REGEXP = /^(eval@)?(\[native code])?$/; const ErrorStackParser = { /** * Given an Error object, extract the most information from it. */ parse: function(error) { if (!error) { return []; } if ( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore typeof error.stacktrace !== "undefined" || // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore typeof error["opera#sourceloc"] !== "undefined" ) { return this.parseOpera( error ); } else if (error.stack && error.stack.match(CHROME_IE_STACK_REGEXP)) { return this.parseV8OrIE(error); } else if (error.stack) { return this.parseFFOrSafari(error); } else { return []; } }, // Separate line and column numbers from a string of the form: (URI:Line:Column) extractLocation: function(urlLike) { if (urlLike.indexOf(":") === -1) { return [urlLike]; } const regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/; const parts = regExp.exec(urlLike.replace(/[()]/g, "")); if (!parts) throw new Error(`Cannot parse given url: ${urlLike}`); return [parts[1], parts[2] || void 0, parts[3] || void 0]; }, parseV8OrIE: function(error) { const filtered = error.stack.split("\n").filter(function(line) { return !!line.match(CHROME_IE_STACK_REGEXP); }, this); return filtered.map(function(line) { if (line.indexOf("(eval ") > -1) { line = line.replace(/eval code/g, "eval").replace(/(\(eval at [^()]*)|(\),.*$)/g, ""); } let sanitizedLine = line.replace(/^\s+/, "").replace(/\(eval code/g, "("); const location = sanitizedLine.match(/ (\((.+):(\d+):(\d+)\)$)/); sanitizedLine = location ? sanitizedLine.replace(location[0], "") : sanitizedLine; const tokens = sanitizedLine.split(/\s+/).slice(1); const locationParts = this.extractLocation( location ? location[1] : tokens.pop() ); const functionName = tokens.join(" ") || void 0; const fileName = ["eval", "<anonymous>"].indexOf(locationParts[0]) > -1 ? void 0 : locationParts[0]; return new StackFrame({ functionName, fileName, lineNumber: locationParts[1], columnNumber: locationParts[2] }); }, this); }, parseFFOrSafari: function(error) { const filtered = error.stack.split("\n").filter(function(line) { return !line.match(SAFARI_NATIVE_CODE_REGEXP); }, this); return filtered.map(function(line) { if (line.indexOf(" > eval") > -1) { line = line.replace( / line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ":$1" ); } if (line.indexOf("@") === -1 && line.indexOf(":") === -1) { return new StackFrame({ functionName: line }); } else { const functionNameRegex = /((.*".+"[^@]*)?[^@]*)(?:@)/; const matches = line.match(functionNameRegex); const functionName = matches && matches[1] ? matches[1] : void 0; const locationParts = this.extractLocation( line.replace(functionNameRegex, "") ); return new StackFrame({ functionName, fileName: locationParts[0], lineNumber: locationParts[1], columnNumber: locationParts[2] }); } }, this); }, parseOpera: function(e) { if (!e.stacktrace || e.message.indexOf("\n") > -1 && e.message.split("\n").length > e.stacktrace.split("\n").length) { return this.parseOpera9(e); } else if (!e.stack) { return this.parseOpera10(e); } else { return this.parseOpera11(e); } }, parseOpera9: function(e) { const lineRE = /Line (\d+).*script (?:in )?(\S+)/i; const lines = e.message.split("\n"); const result = []; for (let i = 2, len = lines.length; i < len; i += 2) { const match = lineRE.exec(lines[i]); if (match) { result.push( new StackFrame({ fileName: match[2], lineNumber: parseFloat(match[1]) }) ); } } return result; }, parseOpera10: function(e) { const lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i; const lines = e.stacktrace.split("\n"); const result = []; for (let i = 0, len = lines.length; i < len; i += 2) { const match = lineRE.exec(lines[i]); if (match) { result.push( new StackFrame({ functionName: match[3] || void 0, fileName: match[2], lineNumber: parseFloat(match[1]) }) ); } } return result; }, // Opera 10.65+ Error.stack very similar to FF/Safari parseOpera11: function(error) { const filtered = error.stack.split("\n").filter(function(line) { return !!line.match(FIREFOX_SAFARI_STACK_REGEXP) && !line.match(/^Error created at/); }, this); return filtered.map(function(line) { const tokens = line.split("@"); const locationParts = this.extractLocation(tokens.pop()); const functionCall = tokens.shift() || ""; const functionName = functionCall.replace(/<anonymous function(: (\w+))?>/, "$2").replace(/\([^)]*\)/g, "") || void 0; return new StackFrame({ functionName, fileName: locationParts[0], lineNumber: locationParts[1], columnNumber: locationParts[2] }); }, this); } }; function pathToSelector(node) { if (!node || !node.outerHTML) { return ""; } let path = ""; while (node.parentElement) { let name = node.localName; if (!name) { break; } name = name.toLowerCase(); const parent = node.parentElement; const domSiblings = []; if (parent.children && parent.children.length > 0) { for (let i = 0; i < parent.children.length; i++) { const sibling = parent.children[i]; if (sibling.localName && sibling.localName.toLowerCase) { if (sibling.localName.toLowerCase() === name) { domSiblings.push(sibling); } } } } if (domSiblings.length > 1) { name += `:eq(${domSiblings.indexOf(node)})`; } path = name + (path ? ">" + path : ""); node = parent; } return path; } function isObject(obj) { return Object.prototype.toString.call(obj) === "[object Object]"; } function isObjTooDeep(obj, limit) { if (limit === 0) { return true; } const keys = Object.keys(obj); for (const key of keys) { if (isObject(obj[key]) && isObjTooDeep(obj[key], limit - 1)) { return true; } } return false; } function stringify(obj, stringifyOptions) { const options = { numOfKeysLimit: 50, depthOfLimit: 4 }; Object.assign(options, stringifyOptions); const stack = []; const keys = []; return JSON.stringify( obj, function(key, value) { if (stack.length > 0) { const thisPos = stack.indexOf(this); ~thisPos ? stack.splice(thisPos + 1) : stack.push(this); ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key); if (~stack.indexOf(value)) { if (stack[0] === value) { value = "[Circular ~]"; } else { value = "[Circular ~." + keys.slice(0, stack.indexOf(value)).join(".") + "]"; } } } else { stack.push(value); } if (value === null) return value; if (value === void 0) return "undefined"; if (shouldIgnore(value)) { return toString(value); } if (typeof value === "bigint") { return value.toString() + "n"; } if (value instanceof Event) { const eventResult = {}; for (const eventKey in value) { const eventValue = value[eventKey]; if (Array.isArray(eventValue)) { eventResult[eventKey] = pathToSelector( eventValue.length ? eventValue[0] : null ); } else { eventResult[eventKey] = eventValue; } } return eventResult; } else if (value instanceof Node) { if (value instanceof HTMLElement) { return value ? value.outerHTML : ""; } return value.nodeName; } else if (value instanceof Error) { return value.stack ? value.stack + "\nEnd of stack for Error object" : value.name + ": " + value.message; } return value; } ); function shouldIgnore(_obj) { if (isObject(_obj) && Object.keys(_obj).length > options.numOfKeysLimit) { return true; } if (typeof _obj === "function") { return true; } if (isObject(_obj) && isObjTooDeep(_obj, options.depthOfLimit)) { return true; } return false; } function toString(_obj) { let str = _obj.toString(); if (options.stringLengthLimit && str.length > options.stringLengthLimit) { str = `${str.slice(0, options.stringLengthLimit)}...`; } return str; } } const defaultLogOptions = { level: [ "assert", "clear", "count", "countReset", "debug", "dir", "dirxml", "error", "group", "groupCollapsed", "groupEnd", "info", "log", "table", "time", "timeEnd", "timeLog", "trace", "warn" ], lengthThreshold: 1e3, logger: "console" }; function initLogObserver(cb, win, options) { const logOptions = options ? Object.assign({}, defaultLogOptions, options) : defaultLogOptions; const loggerType = logOptions.logger; if (!loggerType) { return () => { }; } let logger; if (typeof loggerType === "string") { logger = win[loggerType]; } else { logger = loggerType; } let logCount = 0; let inStack = false; const cancelHandlers = []; if (logOptions.level.includes("error")) { const errorHandler = (event) => { const message = event.message, error = event.error; const trace = ErrorStackParser.parse(error).map( (stackFrame) => stackFrame.toString() ); const payload = [stringify(message, logOptions.stringifyOptions)]; cb({ level: "error", trace, payload }); }; win.addEventListener("error", errorHandler); cancelHandlers.push(() => { win.removeEventListener("error", errorHandler); }); const unhandledrejectionHandler = (event) => { let error; let payload; if (event.reason instanceof Error) { error = event.reason; payload = [ stringify( `Uncaught (in promise) ${error.name}: ${error.message}`, logOptions.stringifyOptions ) ]; } else { error = new Error(); payload = [ stringify("Uncaught (in promise)", logOptions.stringifyOptions), stringify(event.reason, logOptions.stringifyOptions) ]; } const trace = ErrorStackParser.parse(error).map( (stackFrame) => stackFrame.toString() ); cb({ level: "error", trace, payload }); }; win.addEventListener("unhandledrejection", unhandledrejectionHandler); cancelHandlers.push(() => { win.removeEventListener("unhandledrejection", unhandledrejectionHandler); }); } for (const levelType of logOptions.level) { cancelHandlers.push(replace(logger, levelType)); } return () => { cancelHandlers.forEach((h) => h()); }; function replace(_logger, level) { if (!_logger[level]) { return () => { }; } return patch( _logger, level, (original) => { return (...args) => { original.apply(this, args); if (level === "assert" && !!args[0]) { return; } if (inStack) { return; } inStack = true; try { const trace = ErrorStackParser.parse(new Error()).map((stackFrame) => stackFrame.toString()).splice(1); const argsForPayload = level === "assert" ? args.slice(1) : args; const payload = argsForPayload.map( (s) => stringify(s, logOptions.stringifyOptions) ); logCount++; if (logCount < logOptions.lengthThreshold) { cb({ level, trace, payload }); } else if (logCount === logOptions.lengthThreshold) { cb({ level: "warn", trace: [], payload: [ stringify("The number of log records reached the threshold.") ] }); } } catch (error) { original("rrweb logger error:", error, ...args); } finally { inStack = false; } }; } ); } } const PLUGIN_NAME = "rrweb/console@1"; const getRecordConsolePlugin = (options) => ({ name: PLUGIN_NAME, observer: initLogObserver, options }); exports.PLUGIN_NAME = PLUGIN_NAME; exports.getRecordConsolePlugin = getRecordConsolePlugin; //# sourceMappingURL=rrweb-plugin-console-record.cjs.map