303 lines
10 KiB
JavaScript
303 lines
10 KiB
JavaScript
|
Object.defineProperty(exports, '__esModule', { value: true });
|
||
|
|
||
|
const is = require('./is.js');
|
||
|
const memo = require('./memo.js');
|
||
|
const object = require('./object.js');
|
||
|
const stacktrace = require('./stacktrace.js');
|
||
|
|
||
|
/**
|
||
|
* Recursively normalizes the given object.
|
||
|
*
|
||
|
* - Creates a copy to prevent original input mutation
|
||
|
* - Skips non-enumerable properties
|
||
|
* - When stringifying, calls `toJSON` if implemented
|
||
|
* - Removes circular references
|
||
|
* - Translates non-serializable values (`undefined`/`NaN`/functions) to serializable format
|
||
|
* - Translates known global objects/classes to a string representations
|
||
|
* - Takes care of `Error` object serialization
|
||
|
* - Optionally limits depth of final output
|
||
|
* - Optionally limits number of properties/elements included in any single object/array
|
||
|
*
|
||
|
* @param input The object to be normalized.
|
||
|
* @param depth The max depth to which to normalize the object. (Anything deeper stringified whole.)
|
||
|
* @param maxProperties The max number of elements or properties to be included in any single array or
|
||
|
* object in the normallized output.
|
||
|
* @returns A normalized version of the object, or `"**non-serializable**"` if any errors are thrown during normalization.
|
||
|
*/
|
||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
function normalize(input, depth = 100, maxProperties = +Infinity) {
|
||
|
try {
|
||
|
// since we're at the outermost level, we don't provide a key
|
||
|
return visit('', input, depth, maxProperties);
|
||
|
} catch (err) {
|
||
|
return { ERROR: `**non-serializable** (${err})` };
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** JSDoc */
|
||
|
function normalizeToSize(
|
||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
object,
|
||
|
// Default Node.js REPL depth
|
||
|
depth = 3,
|
||
|
// 100kB, as 200kB is max payload size, so half sounds reasonable
|
||
|
maxSize = 100 * 1024,
|
||
|
) {
|
||
|
const normalized = normalize(object, depth);
|
||
|
|
||
|
if (jsonSize(normalized) > maxSize) {
|
||
|
return normalizeToSize(object, depth - 1, maxSize);
|
||
|
}
|
||
|
|
||
|
return normalized ;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Visits a node to perform normalization on it
|
||
|
*
|
||
|
* @param key The key corresponding to the given node
|
||
|
* @param value The node to be visited
|
||
|
* @param depth Optional number indicating the maximum recursion depth
|
||
|
* @param maxProperties Optional maximum number of properties/elements included in any single object/array
|
||
|
* @param memo Optional Memo class handling decycling
|
||
|
*/
|
||
|
function visit(
|
||
|
key,
|
||
|
value,
|
||
|
depth = +Infinity,
|
||
|
maxProperties = +Infinity,
|
||
|
memo$1 = memo.memoBuilder(),
|
||
|
) {
|
||
|
const [memoize, unmemoize] = memo$1;
|
||
|
|
||
|
// Get the simple cases out of the way first
|
||
|
if (
|
||
|
value == null || // this matches null and undefined -> eqeq not eqeqeq
|
||
|
(['number', 'boolean', 'string'].includes(typeof value) && !is.isNaN(value))
|
||
|
) {
|
||
|
return value ;
|
||
|
}
|
||
|
|
||
|
const stringified = stringifyValue(key, value);
|
||
|
|
||
|
// Anything we could potentially dig into more (objects or arrays) will have come back as `"[object XXXX]"`.
|
||
|
// Everything else will have already been serialized, so if we don't see that pattern, we're done.
|
||
|
if (!stringified.startsWith('[object ')) {
|
||
|
return stringified;
|
||
|
}
|
||
|
|
||
|
// From here on, we can assert that `value` is either an object or an array.
|
||
|
|
||
|
// Do not normalize objects that we know have already been normalized. As a general rule, the
|
||
|
// "__sentry_skip_normalization__" property should only be used sparingly and only should only be set on objects that
|
||
|
// have already been normalized.
|
||
|
if ((value )['__sentry_skip_normalization__']) {
|
||
|
return value ;
|
||
|
}
|
||
|
|
||
|
// We can set `__sentry_override_normalization_depth__` on an object to ensure that from there
|
||
|
// We keep a certain amount of depth.
|
||
|
// This should be used sparingly, e.g. we use it for the redux integration to ensure we get a certain amount of state.
|
||
|
const remainingDepth =
|
||
|
typeof (value )['__sentry_override_normalization_depth__'] === 'number'
|
||
|
? ((value )['__sentry_override_normalization_depth__'] )
|
||
|
: depth;
|
||
|
|
||
|
// We're also done if we've reached the max depth
|
||
|
if (remainingDepth === 0) {
|
||
|
// At this point we know `serialized` is a string of the form `"[object XXXX]"`. Clean it up so it's just `"[XXXX]"`.
|
||
|
return stringified.replace('object ', '');
|
||
|
}
|
||
|
|
||
|
// If we've already visited this branch, bail out, as it's circular reference. If not, note that we're seeing it now.
|
||
|
if (memoize(value)) {
|
||
|
return '[Circular ~]';
|
||
|
}
|
||
|
|
||
|
// If the value has a `toJSON` method, we call it to extract more information
|
||
|
const valueWithToJSON = value ;
|
||
|
if (valueWithToJSON && typeof valueWithToJSON.toJSON === 'function') {
|
||
|
try {
|
||
|
const jsonValue = valueWithToJSON.toJSON();
|
||
|
// We need to normalize the return value of `.toJSON()` in case it has circular references
|
||
|
return visit('', jsonValue, remainingDepth - 1, maxProperties, memo$1);
|
||
|
} catch (err) {
|
||
|
// pass (The built-in `toJSON` failed, but we can still try to do it ourselves)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// At this point we know we either have an object or an array, we haven't seen it before, and we're going to recurse
|
||
|
// because we haven't yet reached the max depth. Create an accumulator to hold the results of visiting each
|
||
|
// property/entry, and keep track of the number of items we add to it.
|
||
|
const normalized = (Array.isArray(value) ? [] : {}) ;
|
||
|
let numAdded = 0;
|
||
|
|
||
|
// Before we begin, convert`Error` and`Event` instances into plain objects, since some of each of their relevant
|
||
|
// properties are non-enumerable and otherwise would get missed.
|
||
|
const visitable = object.convertToPlainObject(value );
|
||
|
|
||
|
for (const visitKey in visitable) {
|
||
|
// Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration.
|
||
|
if (!Object.prototype.hasOwnProperty.call(visitable, visitKey)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (numAdded >= maxProperties) {
|
||
|
normalized[visitKey] = '[MaxProperties ~]';
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Recursively visit all the child nodes
|
||
|
const visitValue = visitable[visitKey];
|
||
|
normalized[visitKey] = visit(visitKey, visitValue, remainingDepth - 1, maxProperties, memo$1);
|
||
|
|
||
|
numAdded++;
|
||
|
}
|
||
|
|
||
|
// Once we've visited all the branches, remove the parent from memo storage
|
||
|
unmemoize(value);
|
||
|
|
||
|
// Return accumulated values
|
||
|
return normalized;
|
||
|
}
|
||
|
|
||
|
/* eslint-disable complexity */
|
||
|
/**
|
||
|
* Stringify the given value. Handles various known special values and types.
|
||
|
*
|
||
|
* Not meant to be used on simple primitives which already have a string representation, as it will, for example, turn
|
||
|
* the number 1231 into "[Object Number]", nor on `null`, as it will throw.
|
||
|
*
|
||
|
* @param value The value to stringify
|
||
|
* @returns A stringified representation of the given value
|
||
|
*/
|
||
|
function stringifyValue(
|
||
|
key,
|
||
|
// this type is a tiny bit of a cheat, since this function does handle NaN (which is technically a number), but for
|
||
|
// our internal use, it'll do
|
||
|
value,
|
||
|
) {
|
||
|
try {
|
||
|
if (key === 'domain' && value && typeof value === 'object' && (value )._events) {
|
||
|
return '[Domain]';
|
||
|
}
|
||
|
|
||
|
if (key === 'domainEmitter') {
|
||
|
return '[DomainEmitter]';
|
||
|
}
|
||
|
|
||
|
// It's safe to use `global`, `window`, and `document` here in this manner, as we are asserting using `typeof` first
|
||
|
// which won't throw if they are not present.
|
||
|
|
||
|
if (typeof global !== 'undefined' && value === global) {
|
||
|
return '[Global]';
|
||
|
}
|
||
|
|
||
|
// eslint-disable-next-line no-restricted-globals
|
||
|
if (typeof window !== 'undefined' && value === window) {
|
||
|
return '[Window]';
|
||
|
}
|
||
|
|
||
|
// eslint-disable-next-line no-restricted-globals
|
||
|
if (typeof document !== 'undefined' && value === document) {
|
||
|
return '[Document]';
|
||
|
}
|
||
|
|
||
|
if (is.isVueViewModel(value)) {
|
||
|
return '[VueViewModel]';
|
||
|
}
|
||
|
|
||
|
// React's SyntheticEvent thingy
|
||
|
if (is.isSyntheticEvent(value)) {
|
||
|
return '[SyntheticEvent]';
|
||
|
}
|
||
|
|
||
|
if (typeof value === 'number' && value !== value) {
|
||
|
return '[NaN]';
|
||
|
}
|
||
|
|
||
|
if (typeof value === 'function') {
|
||
|
return `[Function: ${stacktrace.getFunctionName(value)}]`;
|
||
|
}
|
||
|
|
||
|
if (typeof value === 'symbol') {
|
||
|
return `[${String(value)}]`;
|
||
|
}
|
||
|
|
||
|
// stringified BigInts are indistinguishable from regular numbers, so we need to label them to avoid confusion
|
||
|
if (typeof value === 'bigint') {
|
||
|
return `[BigInt: ${String(value)}]`;
|
||
|
}
|
||
|
|
||
|
// Now that we've knocked out all the special cases and the primitives, all we have left are objects. Simply casting
|
||
|
// them to strings means that instances of classes which haven't defined their `toStringTag` will just come out as
|
||
|
// `"[object Object]"`. If we instead look at the constructor's name (which is the same as the name of the class),
|
||
|
// we can make sure that only plain objects come out that way.
|
||
|
const objName = getConstructorName(value);
|
||
|
|
||
|
// Handle HTML Elements
|
||
|
if (/^HTML(\w*)Element$/.test(objName)) {
|
||
|
return `[HTMLElement: ${objName}]`;
|
||
|
}
|
||
|
|
||
|
return `[object ${objName}]`;
|
||
|
} catch (err) {
|
||
|
return `**non-serializable** (${err})`;
|
||
|
}
|
||
|
}
|
||
|
/* eslint-enable complexity */
|
||
|
|
||
|
function getConstructorName(value) {
|
||
|
const prototype = Object.getPrototypeOf(value);
|
||
|
|
||
|
return prototype ? prototype.constructor.name : 'null prototype';
|
||
|
}
|
||
|
|
||
|
/** Calculates bytes size of input string */
|
||
|
function utf8Length(value) {
|
||
|
// eslint-disable-next-line no-bitwise
|
||
|
return ~-encodeURI(value).split(/%..|./).length;
|
||
|
}
|
||
|
|
||
|
/** Calculates bytes size of input object */
|
||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
function jsonSize(value) {
|
||
|
return utf8Length(JSON.stringify(value));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Normalizes URLs in exceptions and stacktraces to a base path so Sentry can fingerprint
|
||
|
* across platforms and working directory.
|
||
|
*
|
||
|
* @param url The URL to be normalized.
|
||
|
* @param basePath The application base path.
|
||
|
* @returns The normalized URL.
|
||
|
*/
|
||
|
function normalizeUrlToBase(url, basePath) {
|
||
|
const escapedBase = basePath
|
||
|
// Backslash to forward
|
||
|
.replace(/\\/g, '/')
|
||
|
// Escape RegExp special characters
|
||
|
.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
|
||
|
|
||
|
let newUrl = url;
|
||
|
try {
|
||
|
newUrl = decodeURI(url);
|
||
|
} catch (_Oo) {
|
||
|
// Sometime this breaks
|
||
|
}
|
||
|
return (
|
||
|
newUrl
|
||
|
.replace(/\\/g, '/')
|
||
|
.replace(/webpack:\/?/g, '') // Remove intermediate base path
|
||
|
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor
|
||
|
.replace(new RegExp(`(file://)?/*${escapedBase}/*`, 'ig'), 'app:///')
|
||
|
);
|
||
|
}
|
||
|
|
||
|
exports.normalize = normalize;
|
||
|
exports.normalizeToSize = normalizeToSize;
|
||
|
exports.normalizeUrlToBase = normalizeUrlToBase;
|
||
|
exports.walk = visit;
|
||
|
//# sourceMappingURL=normalize.js.map
|