1
- import { isPrimitive , isSyntheticEvent } from './is' ;
1
+ import { Primitive } from '@sentry/types' ;
2
+
3
+ import { isError , isEvent , isNaN , isSyntheticEvent } from './is' ;
2
4
import { memoBuilder , MemoFunc } from './memo' ;
3
5
import { convertToPlainObject } from './object' ;
4
6
import { getFunctionName } from './stacktrace' ;
5
7
6
- type UnknownMaybeWithToJson = unknown & { toJSON ?: ( ) => string } ;
7
8
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 } ;
8
14
9
15
/**
10
16
* Recursively normalizes the given object.
@@ -28,7 +34,7 @@ type Prototype = { constructor: (...args: unknown[]) => unknown };
28
34
export function normalize ( input : unknown , depth : number = + Infinity , maxProperties : number = + Infinity ) : any {
29
35
try {
30
36
// 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 ) ;
32
38
} catch ( _oO ) {
33
39
return '**non-serializable**' ;
34
40
}
@@ -52,80 +58,95 @@ export function normalizeToSize<T>(
52
58
}
53
59
54
60
/**
55
- * Walks an object to perform a normalization on it
61
+ * Visits a node to perform normalization on it
56
62
*
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
61
67
* @param memo Optional Memo class handling decycling
62
68
*/
63
- export function walk (
69
+ function visit (
64
70
key : string ,
65
- value : UnknownMaybeWithToJson ,
71
+ value : unknown ,
66
72
depth : number = + Infinity ,
67
73
maxProperties : number = + Infinity ,
68
74
memo : MemoFunc = memoBuilder ( ) ,
69
- ) : unknown {
75
+ ) : Primitive | ObjOrArray < unknown > {
70
76
const [ memoize , unmemoize ] = memo ;
71
77
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 heavy lifting
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
+ // Well, *that* didn't work. Guess we'll have to do it ourselves.
85
+ }
75
86
}
76
87
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 ;
80
91
}
81
92
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 ) ;
89
94
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
+ }
93
100
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
+ }
96
106
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.
98
108
if ( memoize ( value ) ) {
99
109
return '[Circular ~]' ;
100
110
}
101
111
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 ) {
105
123
// 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 ) ) {
107
125
continue ;
108
126
}
109
127
110
- if ( propertyCount >= maxProperties ) {
111
- acc [ innerKey ] = '[MaxProperties ~]' ;
128
+ if ( numAdded >= maxProperties ) {
129
+ normalized [ visitKey ] = '[MaxProperties ~]' ;
112
130
break ;
113
131
}
114
132
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 ) ;
116
136
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 ;
120
138
}
121
139
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
123
141
unmemoize ( value ) ;
124
142
125
143
// Return accumulated values
126
- return acc ;
144
+ return normalized ;
127
145
}
128
146
147
+ // TODO remove this in v7 (this means the method will no longer be exported, under any name)
148
+ export { visit as walk } ;
149
+
129
150
/**
130
151
* Stringify the given value. Handles various known special values and types.
131
152
*
0 commit comments