Skip to content

Commit

Permalink
get walk out of there!
Browse files Browse the repository at this point in the history
  • Loading branch information
timfish committed Mar 7, 2022
1 parent 4a1b499 commit 87f4980
Showing 1 changed file with 79 additions and 64 deletions.
143 changes: 79 additions & 64 deletions packages/utils/src/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ExtendedError, WrappedFunction } from '@sentry/types';

import { htmlTreeAsString } from './browser';
import { isElement, isError, isEvent, isInstanceOf, isPlainObject, isPrimitive, isSyntheticEvent } from './is';
import { memoBuilder } from './memo';
import { memoBuilder, MemoFunc } from './memo';
import { getFunctionName } from './stacktrace';
import { truncate } from './string';

Expand Down Expand Up @@ -302,6 +302,81 @@ function makeSerializable<T>(value: T, key?: any): T | string {

type UnknownMaybeWithToJson = unknown & { toJSON?: () => string };

/**
* Walks an object to perform a normalization on it
*
* @param key of object that's walked in current iteration
* @param value object to be walked
* @param depth Optional number indicating how deep should walking be performed
* @param maxProperties Optional maximum number of properties/elements included in any single object/array
* @param memo Optional Memo class handling decycling
*/
export function walk(
key: string,
value: UnknownMaybeWithToJson,
depth: number = +Infinity,
maxProperties: number = +Infinity,
memo: MemoFunc = memoBuilder(),
): unknown {
const [memoize, unmemoize] = memo;

// If we reach the maximum depth, serialize whatever is left
if (depth === 0) {
return serializeValue(value);
}

// If value implements `toJSON` method, call it and return early
if (value !== null && value !== undefined && typeof value.toJSON === 'function') {
return value.toJSON();
}

// `makeSerializable` provides a string representation of certain non-serializable values. For all others, it's a
// pass-through. If what comes back is a primitive (either because it's been stringified or because it was primitive
// all along), we're done.
const serializable = makeSerializable(value, key);
if (isPrimitive(serializable)) {
return serializable;
}

// Create source that we will use for the next iteration. It will either be an objectified error object (`Error` type
// with extracted key:value pairs) or the input itself.
const source = getWalkSource(value);

// Create an accumulator that will act as a parent for all future itterations of that branch
const acc: { [key: string]: any } = Array.isArray(value) ? [] : {};

// If we already walked that branch, bail out, as it's circular reference
if (memoize(value)) {
return '[Circular ~]';
}

let propertyCount = 0;
// Walk all keys of the source
for (const innerKey in source) {
// Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration.
if (!Object.prototype.hasOwnProperty.call(source, innerKey)) {
continue;
}

if (propertyCount >= maxProperties) {
acc[innerKey] = '[MaxProperties ~]';
break;
}

propertyCount += 1;

// Recursively walk through all the child nodes
const innerValue: UnknownMaybeWithToJson = source[innerKey];
acc[innerKey] = walk(innerKey, innerValue, depth - 1);
}

// Once walked through all the branches, remove the parent from memo storage
unmemoize(value);

// Return accumulated values
return acc;
}

/**
* Recursively normalizes the given object.
*
Expand All @@ -317,74 +392,14 @@ type UnknownMaybeWithToJson = unknown & { toJSON?: () => string };
*
* @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
* @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 normaliztion.
* @returns A normalized version of the object, or `"**non-serializable**"` if any errors are thrown during normalization.
*/
export function normalize(input: unknown, depth: number = +Infinity, maxProperties: number = +Infinity): any {
const [memoize, unmemoize] = memoBuilder();

function walk(key: string, value: UnknownMaybeToJson, depth: number = +Infinity): unknown {
// If we reach the maximum depth, serialize whatever is left
if (depth === 0) {
return serializeValue(value);
}

// If value implements `toJSON` method, call it and return early
if (value !== null && value !== undefined && typeof value.toJSON === 'function') {
return value.toJSON();
}

// `makeSerializable` provides a string representation of certain non-serializable values. For all others, it's a
// pass-through. If what comes back is a primitive (either because it's been stringified or because it was primitive
// all along), we're done.
const serializable = makeSerializable(value, key);
if (isPrimitive(serializable)) {
return serializable;
}

// Create source that we will use for the next iteration. It will either be an objectified error object (`Error` type
// with extracted key:value pairs) or the input itself.
const source = getWalkSource(value);

// Create an accumulator that will act as a parent for all future itterations of that branch
const acc: { [key: string]: any } = Array.isArray(value) ? [] : {};

// If we already walked that branch, bail out, as it's circular reference
if (memoize(value)) {
return '[Circular ~]';
}

let propertyCount = 0;
// Walk all keys of the source
for (const innerKey in source) {
// Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration.
if (!Object.prototype.hasOwnProperty.call(source, innerKey)) {
continue;
}

if (propertyCount >= maxProperties) {
acc[innerKey] = '[MaxProperties ~]';
break;
}

propertyCount += 1;

// Recursively walk through all the child nodes
const innerValue: UnknownMaybeToJson = source[innerKey];
acc[innerKey] = walk(innerKey, innerValue, depth - 1);
}

// Once walked through all the branches, remove the parent from memo storage
unmemoize(value);

// Return accumulated values
return acc;
}

try {
// since we're at the outermost level, there is no key
return walk('', input as UnknownMaybeToJson, depth);
return walk('', input as UnknownMaybeWithToJson, depth, maxProperties);
} catch (_oO) {
return '**non-serializable**';
}
Expand Down

0 comments on commit 87f4980

Please sign in to comment.