@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
JavaScript
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
;