package.build.esm.normalize.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of utils Show documentation
Show all versions of utils Show documentation
Utilities for all Sentry JavaScript SDKs
import { isVueViewModel, isSyntheticEvent } from './is.js';
import { memoBuilder } from './memo.js';
import { convertToPlainObject } from './object.js';
import { getFunctionName } from './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 = memoBuilder(),
) {
const [memoize, unmemoize] = memo;
// 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) && !Number.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);
} 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 = 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);
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 (isVueViewModel(value)) {
return '[VueViewModel]';
}
// React's SyntheticEvent thingy
if (isSyntheticEvent(value)) {
return '[SyntheticEvent]';
}
if (typeof value === 'number' && value !== value) {
return '[NaN]';
}
if (typeof value === 'function') {
return `[Function: ${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:///')
);
}
export { normalize, normalizeToSize, normalizeUrlToBase };
//# sourceMappingURL=normalize.js.map