normalize.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. Object.defineProperty(exports, '__esModule', { value: true });
  2. const is = require('./is.js');
  3. const memo = require('./memo.js');
  4. const object = require('./object.js');
  5. const stacktrace = require('./stacktrace.js');
  6. /**
  7. * Recursively normalizes the given object.
  8. *
  9. * - Creates a copy to prevent original input mutation
  10. * - Skips non-enumerable properties
  11. * - When stringifying, calls `toJSON` if implemented
  12. * - Removes circular references
  13. * - Translates non-serializable values (`undefined`/`NaN`/functions) to serializable format
  14. * - Translates known global objects/classes to a string representations
  15. * - Takes care of `Error` object serialization
  16. * - Optionally limits depth of final output
  17. * - Optionally limits number of properties/elements included in any single object/array
  18. *
  19. * @param input The object to be normalized.
  20. * @param depth The max depth to which to normalize the object. (Anything deeper stringified whole.)
  21. * @param maxProperties The max number of elements or properties to be included in any single array or
  22. * object in the normallized output.
  23. * @returns A normalized version of the object, or `"**non-serializable**"` if any errors are thrown during normalization.
  24. */
  25. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  26. function normalize(input, depth = 100, maxProperties = +Infinity) {
  27. try {
  28. // since we're at the outermost level, we don't provide a key
  29. return visit('', input, depth, maxProperties);
  30. } catch (err) {
  31. return { ERROR: `**non-serializable** (${err})` };
  32. }
  33. }
  34. /** JSDoc */
  35. function normalizeToSize(
  36. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  37. object,
  38. // Default Node.js REPL depth
  39. depth = 3,
  40. // 100kB, as 200kB is max payload size, so half sounds reasonable
  41. maxSize = 100 * 1024,
  42. ) {
  43. const normalized = normalize(object, depth);
  44. if (jsonSize(normalized) > maxSize) {
  45. return normalizeToSize(object, depth - 1, maxSize);
  46. }
  47. return normalized ;
  48. }
  49. /**
  50. * Visits a node to perform normalization on it
  51. *
  52. * @param key The key corresponding to the given node
  53. * @param value The node to be visited
  54. * @param depth Optional number indicating the maximum recursion depth
  55. * @param maxProperties Optional maximum number of properties/elements included in any single object/array
  56. * @param memo Optional Memo class handling decycling
  57. */
  58. function visit(
  59. key,
  60. value,
  61. depth = +Infinity,
  62. maxProperties = +Infinity,
  63. memo$1 = memo.memoBuilder(),
  64. ) {
  65. const [memoize, unmemoize] = memo$1;
  66. // Get the simple cases out of the way first
  67. if (
  68. value == null || // this matches null and undefined -> eqeq not eqeqeq
  69. (['number', 'boolean', 'string'].includes(typeof value) && !is.isNaN(value))
  70. ) {
  71. return value ;
  72. }
  73. const stringified = stringifyValue(key, value);
  74. // Anything we could potentially dig into more (objects or arrays) will have come back as `"[object XXXX]"`.
  75. // Everything else will have already been serialized, so if we don't see that pattern, we're done.
  76. if (!stringified.startsWith('[object ')) {
  77. return stringified;
  78. }
  79. // From here on, we can assert that `value` is either an object or an array.
  80. // Do not normalize objects that we know have already been normalized. As a general rule, the
  81. // "__sentry_skip_normalization__" property should only be used sparingly and only should only be set on objects that
  82. // have already been normalized.
  83. if ((value )['__sentry_skip_normalization__']) {
  84. return value ;
  85. }
  86. // We can set `__sentry_override_normalization_depth__` on an object to ensure that from there
  87. // We keep a certain amount of depth.
  88. // This should be used sparingly, e.g. we use it for the redux integration to ensure we get a certain amount of state.
  89. const remainingDepth =
  90. typeof (value )['__sentry_override_normalization_depth__'] === 'number'
  91. ? ((value )['__sentry_override_normalization_depth__'] )
  92. : depth;
  93. // We're also done if we've reached the max depth
  94. if (remainingDepth === 0) {
  95. // At this point we know `serialized` is a string of the form `"[object XXXX]"`. Clean it up so it's just `"[XXXX]"`.
  96. return stringified.replace('object ', '');
  97. }
  98. // If we've already visited this branch, bail out, as it's circular reference. If not, note that we're seeing it now.
  99. if (memoize(value)) {
  100. return '[Circular ~]';
  101. }
  102. // If the value has a `toJSON` method, we call it to extract more information
  103. const valueWithToJSON = value ;
  104. if (valueWithToJSON && typeof valueWithToJSON.toJSON === 'function') {
  105. try {
  106. const jsonValue = valueWithToJSON.toJSON();
  107. // We need to normalize the return value of `.toJSON()` in case it has circular references
  108. return visit('', jsonValue, remainingDepth - 1, maxProperties, memo$1);
  109. } catch (err) {
  110. // pass (The built-in `toJSON` failed, but we can still try to do it ourselves)
  111. }
  112. }
  113. // 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
  114. // because we haven't yet reached the max depth. Create an accumulator to hold the results of visiting each
  115. // property/entry, and keep track of the number of items we add to it.
  116. const normalized = (Array.isArray(value) ? [] : {}) ;
  117. let numAdded = 0;
  118. // Before we begin, convert`Error` and`Event` instances into plain objects, since some of each of their relevant
  119. // properties are non-enumerable and otherwise would get missed.
  120. const visitable = object.convertToPlainObject(value );
  121. for (const visitKey in visitable) {
  122. // Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration.
  123. if (!Object.prototype.hasOwnProperty.call(visitable, visitKey)) {
  124. continue;
  125. }
  126. if (numAdded >= maxProperties) {
  127. normalized[visitKey] = '[MaxProperties ~]';
  128. break;
  129. }
  130. // Recursively visit all the child nodes
  131. const visitValue = visitable[visitKey];
  132. normalized[visitKey] = visit(visitKey, visitValue, remainingDepth - 1, maxProperties, memo$1);
  133. numAdded++;
  134. }
  135. // Once we've visited all the branches, remove the parent from memo storage
  136. unmemoize(value);
  137. // Return accumulated values
  138. return normalized;
  139. }
  140. /* eslint-disable complexity */
  141. /**
  142. * Stringify the given value. Handles various known special values and types.
  143. *
  144. * Not meant to be used on simple primitives which already have a string representation, as it will, for example, turn
  145. * the number 1231 into "[Object Number]", nor on `null`, as it will throw.
  146. *
  147. * @param value The value to stringify
  148. * @returns A stringified representation of the given value
  149. */
  150. function stringifyValue(
  151. key,
  152. // this type is a tiny bit of a cheat, since this function does handle NaN (which is technically a number), but for
  153. // our internal use, it'll do
  154. value,
  155. ) {
  156. try {
  157. if (key === 'domain' && value && typeof value === 'object' && (value )._events) {
  158. return '[Domain]';
  159. }
  160. if (key === 'domainEmitter') {
  161. return '[DomainEmitter]';
  162. }
  163. // It's safe to use `global`, `window`, and `document` here in this manner, as we are asserting using `typeof` first
  164. // which won't throw if they are not present.
  165. if (typeof global !== 'undefined' && value === global) {
  166. return '[Global]';
  167. }
  168. // eslint-disable-next-line no-restricted-globals
  169. if (typeof window !== 'undefined' && value === window) {
  170. return '[Window]';
  171. }
  172. // eslint-disable-next-line no-restricted-globals
  173. if (typeof document !== 'undefined' && value === document) {
  174. return '[Document]';
  175. }
  176. if (is.isVueViewModel(value)) {
  177. return '[VueViewModel]';
  178. }
  179. // React's SyntheticEvent thingy
  180. if (is.isSyntheticEvent(value)) {
  181. return '[SyntheticEvent]';
  182. }
  183. if (typeof value === 'number' && value !== value) {
  184. return '[NaN]';
  185. }
  186. if (typeof value === 'function') {
  187. return `[Function: ${stacktrace.getFunctionName(value)}]`;
  188. }
  189. if (typeof value === 'symbol') {
  190. return `[${String(value)}]`;
  191. }
  192. // stringified BigInts are indistinguishable from regular numbers, so we need to label them to avoid confusion
  193. if (typeof value === 'bigint') {
  194. return `[BigInt: ${String(value)}]`;
  195. }
  196. // Now that we've knocked out all the special cases and the primitives, all we have left are objects. Simply casting
  197. // them to strings means that instances of classes which haven't defined their `toStringTag` will just come out as
  198. // `"[object Object]"`. If we instead look at the constructor's name (which is the same as the name of the class),
  199. // we can make sure that only plain objects come out that way.
  200. const objName = getConstructorName(value);
  201. // Handle HTML Elements
  202. if (/^HTML(\w*)Element$/.test(objName)) {
  203. return `[HTMLElement: ${objName}]`;
  204. }
  205. return `[object ${objName}]`;
  206. } catch (err) {
  207. return `**non-serializable** (${err})`;
  208. }
  209. }
  210. /* eslint-enable complexity */
  211. function getConstructorName(value) {
  212. const prototype = Object.getPrototypeOf(value);
  213. return prototype ? prototype.constructor.name : 'null prototype';
  214. }
  215. /** Calculates bytes size of input string */
  216. function utf8Length(value) {
  217. // eslint-disable-next-line no-bitwise
  218. return ~-encodeURI(value).split(/%..|./).length;
  219. }
  220. /** Calculates bytes size of input object */
  221. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  222. function jsonSize(value) {
  223. return utf8Length(JSON.stringify(value));
  224. }
  225. /**
  226. * Normalizes URLs in exceptions and stacktraces to a base path so Sentry can fingerprint
  227. * across platforms and working directory.
  228. *
  229. * @param url The URL to be normalized.
  230. * @param basePath The application base path.
  231. * @returns The normalized URL.
  232. */
  233. function normalizeUrlToBase(url, basePath) {
  234. const escapedBase = basePath
  235. // Backslash to forward
  236. .replace(/\\/g, '/')
  237. // Escape RegExp special characters
  238. .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
  239. let newUrl = url;
  240. try {
  241. newUrl = decodeURI(url);
  242. } catch (_Oo) {
  243. // Sometime this breaks
  244. }
  245. return (
  246. newUrl
  247. .replace(/\\/g, '/')
  248. .replace(/webpack:\/?/g, '') // Remove intermediate base path
  249. // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor
  250. .replace(new RegExp(`(file://)?/*${escapedBase}/*`, 'ig'), 'app:///')
  251. );
  252. }
  253. exports.normalize = normalize;
  254. exports.normalizeToSize = normalizeToSize;
  255. exports.normalizeUrlToBase = normalizeUrlToBase;
  256. exports.walk = visit;
  257. //# sourceMappingURL=normalize.js.map