Skip to content

Commit b878e82

Browse files
committed
rename and refactor walk
1 parent 28f9331 commit b878e82

File tree

1 file changed

+63
-42
lines changed

1 file changed

+63
-42
lines changed

packages/utils/src/normalize.ts

Lines changed: 63 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
import { isPrimitive, isSyntheticEvent } from './is';
1+
import { Primitive } from '@sentry/types';
2+
3+
import { isError, isEvent, isNaN, isSyntheticEvent } from './is';
24
import { memoBuilder, MemoFunc } from './memo';
35
import { convertToPlainObject } from './object';
46
import { getFunctionName } from './stacktrace';
57

6-
type UnknownMaybeWithToJson = unknown & { toJSON?: () => string };
78
type Prototype = { constructor: (...args: unknown[]) => unknown };
9+
// This is a hack to placate TS, relying on the fact that technically, arrays are objects with integer keys. Normally we
10+
// think of those keys as actual numbers, but `arr['0']` turns out to work just as well as `arr[0]`, and doing it this
11+
// way lets us use a single type in the places where behave as if we are only dealing with objects, even if some of them
12+
// might be arrays.
13+
type ObjOrArray<T> = { [key: string]: T };
814

915
/**
1016
* Recursively normalizes the given object.
@@ -28,7 +34,7 @@ type Prototype = { constructor: (...args: unknown[]) => unknown };
2834
export function normalize(input: unknown, depth: number = +Infinity, maxProperties: number = +Infinity): any {
2935
try {
3036
// since we're at the outermost level, there is no key
31-
return walk('', input as UnknownMaybeWithToJson, depth, maxProperties);
37+
return visit('', input, depth, maxProperties);
3238
} catch (_oO) {
3339
return '**non-serializable**';
3440
}
@@ -52,80 +58,95 @@ export function normalizeToSize<T>(
5258
}
5359

5460
/**
55-
* Walks an object to perform a normalization on it
61+
* Visits a node to perform normalization on it
5662
*
57-
* @param key of object that's walked in current iteration
58-
* @param value object to be walked
59-
* @param depth Optional number indicating how deep should walking be performed
60-
* @param maxProperties Optional maximum number of properties/elements included in any single object/array
63+
* @param key The key corresponding to the given node
64+
* @param value The node to be visited
65+
* @param depth Optional number indicating the maximum recursion depth
66+
* @param maxProperties Optional maximum number of properties/elements included in any single object/array
6167
* @param memo Optional Memo class handling decycling
6268
*/
63-
export function walk(
69+
function visit(
6470
key: string,
65-
value: UnknownMaybeWithToJson,
71+
value: unknown,
6672
depth: number = +Infinity,
6773
maxProperties: number = +Infinity,
6874
memo: MemoFunc = memoBuilder(),
69-
): unknown {
75+
): Primitive | ObjOrArray<unknown> {
7076
const [memoize, unmemoize] = memo;
7177

72-
// If we reach the maximum depth, serialize whatever is left
73-
if (depth === 0) {
74-
return stringifyValue(key, value);
78+
// If the value has a `toJSON` method, see if we can bail and let it do the work
79+
const valueWithToJSON = value as unknown & { toJSON?: () => Primitive | ObjOrArray<unknown> };
80+
if (valueWithToJSON && typeof valueWithToJSON.toJSON === 'function') {
81+
try {
82+
return valueWithToJSON.toJSON();
83+
} catch (err) {
84+
// pass (The built-in `toJSON` failed, but we can still try to do it ourselves)
85+
}
7586
}
7687

77-
// If value implements `toJSON` method, call it and return early
78-
if (value !== null && value !== undefined && typeof value.toJSON === 'function') {
79-
return value.toJSON();
88+
// Get the simple cases out of the way first
89+
if (value === null || (['number', 'boolean', 'string'].includes(typeof value) && !isNaN(value))) {
90+
return value as Primitive;
8091
}
8192

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

90-
// Create source that we will use for the next iteration. It will either be an objectified error object (`Error` type
91-
// with extracted key:value pairs) or the input itself.
92-
const source = convertToPlainObject(value);
95+
// Anything we could potentially dig into more (objects or arrays) will have come back as `"[object XXXX]"`.
96+
// Everything else will have already been serialized, so if we don't see that pattern, we're done.
97+
if (!stringified.startsWith('[object ')) {
98+
return stringified;
99+
}
93100

94-
// Create an accumulator that will act as a parent for all future itterations of that branch
95-
const acc: { [key: string]: any } = Array.isArray(value) ? [] : {};
101+
// We're also done if we've reached the max depth
102+
if (depth === 0) {
103+
// At this point we know `serialized` is a string of the form `"[object XXXX]"`. Clean it up so it's just `"[XXXX]"`.
104+
return stringified.replace('object ', '');
105+
}
96106

97-
// If we already walked that branch, bail out, as it's circular reference
107+
// If we've already visited this branch, bail out, as it's circular reference. If not, note that we're seeing it now.
98108
if (memoize(value)) {
99109
return '[Circular ~]';
100110
}
101111

102-
let propertyCount = 0;
103-
// Walk all keys of the source
104-
for (const innerKey in source) {
112+
// 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
113+
// because we haven't yet reached the max depth. Create an accumulator to hold the results of visiting each
114+
// property/entry, and keep track of the number of items we add to it.
115+
const normalized = (Array.isArray(value) ? [] : {}) as ObjOrArray<unknown>;
116+
let numAdded = 0;
117+
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 = (isError(value) || isEvent(value) ? convertToPlainObject(value) : value) as ObjOrArray<unknown>;
121+
122+
for (const visitKey in visitable) {
105123
// Avoid iterating over fields in the prototype if they've somehow been exposed to enumeration.
106-
if (!Object.prototype.hasOwnProperty.call(source, innerKey)) {
124+
if (!Object.prototype.hasOwnProperty.call(visitable, visitKey)) {
107125
continue;
108126
}
109127

110-
if (propertyCount >= maxProperties) {
111-
acc[innerKey] = '[MaxProperties ~]';
128+
if (numAdded >= maxProperties) {
129+
normalized[visitKey] = '[MaxProperties ~]';
112130
break;
113131
}
114132

115-
propertyCount += 1;
133+
// Recursively visit all the child nodes
134+
const visitValue = visitable[visitKey];
135+
normalized[visitKey] = visit(visitKey, visitValue, depth - 1, maxProperties, memo);
116136

117-
// Recursively walk through all the child nodes
118-
const innerValue = source[innerKey] as UnknownMaybeWithToJson;
119-
acc[innerKey] = walk(innerKey, innerValue, depth - 1, maxProperties, memo);
137+
numAdded += 1;
120138
}
121139

122-
// Once walked through all the branches, remove the parent from memo storage
140+
// Once we've visited all the branches, remove the parent from memo storage
123141
unmemoize(value);
124142

125143
// Return accumulated values
126-
return acc;
144+
return normalized;
127145
}
128146

147+
// TODO remove this in v7 (this means the method will no longer be exported, under any name)
148+
export { visit as walk };
149+
129150
/**
130151
* Stringify the given value. Handles various known special values and types.
131152
*

0 commit comments

Comments
 (0)